mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
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:
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { ActionIcon, IconTrash } from "@homarr/ui";
|
||||
|
||||
import { revalidatePathAction } from "../../../revalidatePathAction";
|
||||
import { modalEvents } from "../../modals";
|
||||
|
||||
interface AppDeleteButtonProps {
|
||||
app: RouterOutputs["app"]["all"][number];
|
||||
}
|
||||
|
||||
export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
|
||||
const t = useScopedI18n("app.page.delete");
|
||||
const { mutate, isPending } = clientApi.app.delete.useMutation();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
modalEvents.openConfirmModal({
|
||||
title: t("title"),
|
||||
children: t("message", app),
|
||||
onConfirm: () => {
|
||||
mutate(
|
||||
{ id: app.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSuccessNotification({
|
||||
title: t("notification.success.title"),
|
||||
message: t("notification.success.message"),
|
||||
});
|
||||
void revalidatePathAction("/apps");
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: t("notification.error.title"),
|
||||
message: t("notification.error.message"),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
}, [app, mutate, t]);
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
loading={isPending}
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={onClick}
|
||||
aria-label="Delete app"
|
||||
>
|
||||
<IconTrash color="red" size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
60
apps/nextjs/src/app/[locale]/(main)/apps/_form.tsx
Normal file
60
apps/nextjs/src/app/[locale]/(main)/apps/_form.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { useForm, zodResolver } from "@homarr/form";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Button, Group, Stack, Textarea, TextInput } from "@homarr/ui";
|
||||
import type { z } from "@homarr/validation";
|
||||
import { validation } from "@homarr/validation";
|
||||
|
||||
// TODO: add icon picker
|
||||
type FormType = z.infer<typeof validation.app.manage>;
|
||||
|
||||
interface AppFormProps {
|
||||
submitButtonTranslation: (t: TranslationFunction) => string;
|
||||
initialValues?: FormType;
|
||||
handleSubmit: (values: FormType) => void;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
export const AppForm = (props: AppFormProps) => {
|
||||
const { submitButtonTranslation, handleSubmit, initialValues, isPending } =
|
||||
props;
|
||||
const t = useI18n();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues ?? {
|
||||
name: "",
|
||||
description: "",
|
||||
iconUrl: "",
|
||||
href: "",
|
||||
},
|
||||
validate: zodResolver(validation.app.manage),
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput {...form.getInputProps("name")} withAsterisk label="Name" />
|
||||
<TextInput
|
||||
{...form.getInputProps("iconUrl")}
|
||||
withAsterisk
|
||||
label="Icon URL"
|
||||
/>
|
||||
<Textarea {...form.getInputProps("description")} label="Description" />
|
||||
<TextInput {...form.getInputProps("href")} label="URL" />
|
||||
|
||||
<Group justify="end">
|
||||
<Button variant="default" component={Link} href="/apps">
|
||||
{t("common.action.backToOverview")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{submitButtonTranslation(t)}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
||||
import { AppForm } from "../../_form";
|
||||
|
||||
interface AppEditFormProps {
|
||||
app: RouterOutputs["app"]["byId"];
|
||||
}
|
||||
|
||||
export const AppEditForm = ({ app }: AppEditFormProps) => {
|
||||
const t = useScopedI18n("app.page.edit.notification");
|
||||
const router = useRouter();
|
||||
|
||||
const { mutate, isPending } = clientApi.app.update.useMutation({
|
||||
onSuccess: () => {
|
||||
showSuccessNotification({
|
||||
title: t("success.title"),
|
||||
message: t("success.message"),
|
||||
});
|
||||
void revalidatePathAction("/apps").then(() => {
|
||||
router.push("/apps");
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: t("error.title"),
|
||||
message: t("error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: z.infer<typeof validation.app.manage>) => {
|
||||
mutate({
|
||||
id: app.id,
|
||||
...values,
|
||||
});
|
||||
},
|
||||
[mutate, app.id],
|
||||
);
|
||||
|
||||
const submitButtonTranslation = useCallback(
|
||||
(t: TranslationFunction) => t("common.action.save"),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<AppForm
|
||||
submitButtonTranslation={submitButtonTranslation}
|
||||
initialValues={app}
|
||||
handleSubmit={handleSubmit}
|
||||
isPending={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
23
apps/nextjs/src/app/[locale]/(main)/apps/edit/[id]/page.tsx
Normal file
23
apps/nextjs/src/app/[locale]/(main)/apps/edit/[id]/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import { Container, Stack, Title } from "@homarr/ui";
|
||||
|
||||
import { api } from "~/trpc/server";
|
||||
import { AppEditForm } from "./_app-edit-form";
|
||||
|
||||
interface AppEditPageProps {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
export default async function AppEditPage({ params }: AppEditPageProps) {
|
||||
const app = await api.app.byId({ id: params.id });
|
||||
const t = await getI18n();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Stack>
|
||||
<Title>{t("app.page.edit.title")}</Title>
|
||||
<AppEditForm app={app} />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
showErrorNotification,
|
||||
showSuccessNotification,
|
||||
} from "@homarr/notifications";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { validation, z } from "@homarr/validation";
|
||||
|
||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
||||
import { AppForm } from "../_form";
|
||||
|
||||
export const AppNewForm = () => {
|
||||
const t = useScopedI18n("app.page.create.notification");
|
||||
const router = useRouter();
|
||||
|
||||
const { mutate, isPending } = clientApi.app.create.useMutation({
|
||||
onSuccess: () => {
|
||||
showSuccessNotification({
|
||||
title: t("success.title"),
|
||||
message: t("success.message"),
|
||||
});
|
||||
void revalidatePathAction("/apps").then(() => {
|
||||
router.push("/apps");
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: t("error.title"),
|
||||
message: t("error.message"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(values: z.infer<typeof validation.app.manage>) => {
|
||||
mutate(values);
|
||||
},
|
||||
[mutate],
|
||||
);
|
||||
|
||||
const submitButtonTranslation = useCallback(
|
||||
(t: TranslationFunction) => t("common.action.create"),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<AppForm
|
||||
submitButtonTranslation={submitButtonTranslation}
|
||||
handleSubmit={handleSubmit}
|
||||
isPending={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
14
apps/nextjs/src/app/[locale]/(main)/apps/new/page.tsx
Normal file
14
apps/nextjs/src/app/[locale]/(main)/apps/new/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Container, Stack, Title } from "@homarr/ui";
|
||||
|
||||
import { AppNewForm } from "./_app-new-form";
|
||||
|
||||
export default function AppNewPage() {
|
||||
return (
|
||||
<Container>
|
||||
<Stack>
|
||||
<Title>New app</Title>
|
||||
<AppNewForm />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
117
apps/nextjs/src/app/[locale]/(main)/apps/page.tsx
Normal file
117
apps/nextjs/src/app/[locale]/(main)/apps/page.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { getI18n } from "@homarr/translation/server";
|
||||
import {
|
||||
ActionIcon,
|
||||
ActionIconGroup,
|
||||
Anchor,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Group,
|
||||
IconApps,
|
||||
IconPencil,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@homarr/ui";
|
||||
|
||||
import { api } from "~/trpc/server";
|
||||
import { AppDeleteButton } from "./_app-delete-button";
|
||||
|
||||
export default async function AppsPage() {
|
||||
const apps = await api.app.all();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Stack>
|
||||
<Group justify="space-between" align="center">
|
||||
<Title>Apps</Title>
|
||||
<Button component={Link} href="/apps/new">
|
||||
New app
|
||||
</Button>
|
||||
</Group>
|
||||
{apps.length === 0 && <AppNoResults />}
|
||||
{apps.length > 0 && (
|
||||
<Stack gap="sm">
|
||||
{apps.map((app) => (
|
||||
<AppCard key={app.id} app={app} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface AppCardProps {
|
||||
app: RouterOutputs["app"]["all"][number];
|
||||
}
|
||||
|
||||
const AppCard = ({ app }: AppCardProps) => {
|
||||
return (
|
||||
<Card>
|
||||
<Group justify="space-between">
|
||||
<Group align="top" justify="start" wrap="nowrap">
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={app.iconUrl}
|
||||
radius={0}
|
||||
styles={{
|
||||
image: {
|
||||
objectFit: "contain",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack gap={0}>
|
||||
<Text fw={500}>{app.name}</Text>
|
||||
{app.description && (
|
||||
<Text size="sm" c="gray.6">
|
||||
{app.description}
|
||||
</Text>
|
||||
)}
|
||||
{app.href && (
|
||||
<Anchor href={app.href} size="sm" w="min-content">
|
||||
{app.href}
|
||||
</Anchor>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group>
|
||||
<ActionIconGroup>
|
||||
<ActionIcon
|
||||
component={Link}
|
||||
href={`/apps/edit/${app.id}`}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label="Edit app"
|
||||
>
|
||||
<IconPencil size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
<AppDeleteButton app={app} />
|
||||
</ActionIconGroup>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const AppNoResults = async () => {
|
||||
const t = await getI18n();
|
||||
|
||||
return (
|
||||
<Card withBorder bg="transparent">
|
||||
<Stack align="center" gap="sm">
|
||||
<IconApps size="2rem" />
|
||||
<Text fw={500} size="lg">
|
||||
{t("app.page.list.noResults.title")}
|
||||
</Text>
|
||||
<Anchor href="/apps/new">
|
||||
{t("app.page.list.noResults.description")}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
71
packages/api/src/router/app.ts
Normal file
71
packages/api/src/router/app.ts
Normal 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));
|
||||
}),
|
||||
});
|
||||
209
packages/api/src/router/test/app.spec.ts
Normal file
209
packages/api/src/router/test/app.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
7
packages/db/migrations/0001_slim_swarm.sql
Normal file
7
packages/db/migrations/0001_slim_swarm.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE `app` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`icon_url` text NOT NULL,
|
||||
`href` text
|
||||
);
|
||||
722
packages/db/migrations/meta/0001_snapshot.json
Normal file
722
packages/db/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,722 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "sqlite",
|
||||
"id": "f7263224-116a-42ba-8fb1-4574cb637880",
|
||||
"prevId": "7ba12cbf-d8eb-4469-b7c5-7f31acb7717d",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"providerAccountId": {
|
||||
"name": "providerAccountId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token_type": {
|
||||
"name": "token_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"userId_idx": {
|
||||
"name": "userId_idx",
|
||||
"columns": ["userId"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"account_userId_user_id_fk": {
|
||||
"name": "account_userId_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["userId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"account_provider_providerAccountId_pk": {
|
||||
"columns": ["provider", "providerAccountId"],
|
||||
"name": "account_provider_providerAccountId_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"app": {
|
||||
"name": "app",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon_url": {
|
||||
"name": "icon_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"href": {
|
||||
"name": "href",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"board": {
|
||||
"name": "board",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_public": {
|
||||
"name": "is_public",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"page_title": {
|
||||
"name": "page_title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"meta_title": {
|
||||
"name": "meta_title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"logo_image_url": {
|
||||
"name": "logo_image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"favicon_image_url": {
|
||||
"name": "favicon_image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"background_image_url": {
|
||||
"name": "background_image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"background_image_attachment": {
|
||||
"name": "background_image_attachment",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'fixed'"
|
||||
},
|
||||
"background_image_repeat": {
|
||||
"name": "background_image_repeat",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'no-repeat'"
|
||||
},
|
||||
"background_image_size": {
|
||||
"name": "background_image_size",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'cover'"
|
||||
},
|
||||
"primary_color": {
|
||||
"name": "primary_color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'#fa5252'"
|
||||
},
|
||||
"secondary_color": {
|
||||
"name": "secondary_color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'#fd7e14'"
|
||||
},
|
||||
"opacity": {
|
||||
"name": "opacity",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 100
|
||||
},
|
||||
"custom_css": {
|
||||
"name": "custom_css",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"column_count": {
|
||||
"name": "column_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"board_name_unique": {
|
||||
"name": "board_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"integration_item": {
|
||||
"name": "integration_item",
|
||||
"columns": {
|
||||
"item_id": {
|
||||
"name": "item_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"integration_id": {
|
||||
"name": "integration_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"integration_item_item_id_item_id_fk": {
|
||||
"name": "integration_item_item_id_item_id_fk",
|
||||
"tableFrom": "integration_item",
|
||||
"tableTo": "item",
|
||||
"columnsFrom": ["item_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"integration_item_integration_id_integration_id_fk": {
|
||||
"name": "integration_item_integration_id_integration_id_fk",
|
||||
"tableFrom": "integration_item",
|
||||
"tableTo": "integration",
|
||||
"columnsFrom": ["integration_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"integration_item_item_id_integration_id_pk": {
|
||||
"columns": ["integration_id", "item_id"],
|
||||
"name": "integration_item_item_id_integration_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"integrationSecret": {
|
||||
"name": "integrationSecret",
|
||||
"columns": {
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"integration_id": {
|
||||
"name": "integration_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"integration_secret__kind_idx": {
|
||||
"name": "integration_secret__kind_idx",
|
||||
"columns": ["kind"],
|
||||
"isUnique": false
|
||||
},
|
||||
"integration_secret__updated_at_idx": {
|
||||
"name": "integration_secret__updated_at_idx",
|
||||
"columns": ["updated_at"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"integrationSecret_integration_id_integration_id_fk": {
|
||||
"name": "integrationSecret_integration_id_integration_id_fk",
|
||||
"tableFrom": "integrationSecret",
|
||||
"tableTo": "integration",
|
||||
"columnsFrom": ["integration_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"integrationSecret_integration_id_kind_pk": {
|
||||
"columns": ["integration_id", "kind"],
|
||||
"name": "integrationSecret_integration_id_kind_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"integration": {
|
||||
"name": "integration",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"integration__kind_idx": {
|
||||
"name": "integration__kind_idx",
|
||||
"columns": ["kind"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"item": {
|
||||
"name": "item",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"section_id": {
|
||||
"name": "section_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"x_offset": {
|
||||
"name": "x_offset",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"y_offset": {
|
||||
"name": "y_offset",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"width": {
|
||||
"name": "width",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"height": {
|
||||
"name": "height",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"options": {
|
||||
"name": "options",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'{\"json\": {}}'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"item_section_id_section_id_fk": {
|
||||
"name": "item_section_id_section_id_fk",
|
||||
"tableFrom": "item",
|
||||
"tableTo": "section",
|
||||
"columnsFrom": ["section_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"section": {
|
||||
"name": "section",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"board_id": {
|
||||
"name": "board_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"position": {
|
||||
"name": "position",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"section_board_id_board_id_fk": {
|
||||
"name": "section_board_id_board_id_fk",
|
||||
"tableFrom": "section",
|
||||
"tableTo": "board",
|
||||
"columnsFrom": ["board_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
"sessionToken": {
|
||||
"name": "sessionToken",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_id_idx": {
|
||||
"name": "user_id_idx",
|
||||
"columns": ["userId"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"session_userId_user_id_fk": {
|
||||
"name": "session_userId_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["userId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"salt": {
|
||||
"name": "salt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"verificationToken": {
|
||||
"name": "verificationToken",
|
||||
"columns": {
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"verificationToken_identifier_token_pk": {
|
||||
"columns": ["identifier", "token"],
|
||||
"name": "verificationToken_identifier_token_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1709409142712,
|
||||
"tag": "0000_sloppy_bloodstorm",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1709585624230,
|
||||
"tag": "0001_slim_swarm",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -169,6 +169,14 @@ export const items = sqliteTable("item", {
|
||||
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
||||
});
|
||||
|
||||
export const apps = sqliteTable("app", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
iconUrl: text("icon_url").notNull(),
|
||||
href: text("href"),
|
||||
});
|
||||
|
||||
export const integrationItems = sqliteTable(
|
||||
"integration_item",
|
||||
{
|
||||
|
||||
@@ -28,6 +28,57 @@ export default {
|
||||
create: "Create user",
|
||||
},
|
||||
},
|
||||
app: {
|
||||
page: {
|
||||
list: {
|
||||
title: "Apps",
|
||||
noResults: {
|
||||
title: "There aren't any apps.",
|
||||
description: "Create your first app",
|
||||
},
|
||||
},
|
||||
create: {
|
||||
title: "New app",
|
||||
notification: {
|
||||
success: {
|
||||
title: "Creation successful",
|
||||
message: "The app was successfully created",
|
||||
},
|
||||
error: {
|
||||
title: "Creation failed",
|
||||
message: "The app could not be created",
|
||||
},
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
title: "Edit app",
|
||||
notification: {
|
||||
success: {
|
||||
title: "Changes applied successfully",
|
||||
message: "The app was successfully saved",
|
||||
},
|
||||
error: {
|
||||
title: "Unable to apply changes",
|
||||
message: "The app could not be saved",
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
title: "Delete app",
|
||||
message: "Are you sure you want to delete the app {name}?",
|
||||
notification: {
|
||||
success: {
|
||||
title: "Deletion successful",
|
||||
message: "The app was successfully deleted",
|
||||
},
|
||||
error: {
|
||||
title: "Deletion failed",
|
||||
message: "Unable to delete the app",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
page: {
|
||||
list: {
|
||||
|
||||
18
packages/validation/src/app.ts
Normal file
18
packages/validation/src/app.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const manageAppSchema = z.object({
|
||||
name: z.string().min(1).max(64),
|
||||
description: z.string().max(512).nullable(),
|
||||
iconUrl: z.string().min(1),
|
||||
href: z.string().nullable(),
|
||||
});
|
||||
|
||||
const editAppSchema = manageAppSchema.and(z.object({ id: z.string() }));
|
||||
|
||||
const byIdSchema = z.object({ id: z.string() });
|
||||
|
||||
export const appSchemas = {
|
||||
manage: manageAppSchema,
|
||||
edit: editAppSchema,
|
||||
byId: byIdSchema,
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { appSchemas } from "./app";
|
||||
import { boardSchemas } from "./board";
|
||||
import { integrationSchemas } from "./integration";
|
||||
import { userSchemas } from "./user";
|
||||
@@ -6,6 +7,7 @@ export const validation = {
|
||||
user: userSchemas,
|
||||
integration: integrationSchemas,
|
||||
board: boardSchemas,
|
||||
app: appSchemas,
|
||||
};
|
||||
|
||||
export { createSectionSchema, sharedItemSchema } from "./shared";
|
||||
|
||||
Reference in New Issue
Block a user