test: add initial unit tests (#56)
* chore: add initial db migration * test: add unit tests for packages auth, common, widgets * fix: deep source issues * fix: format issues * wip: add unit tests for api routers * fix: deep source issues * test: add missing unit tests for integration router * wip: board tests * test: add unit tests for board router * fix: remove unnecessary null assertions * fix: deepsource issues * fix: formatting * fix: pnpm lock * fix: lint and typecheck issues * chore: address pull request feedback * fix: non-null assertions * fix: lockfile broken
This commit is contained in:
@@ -108,7 +108,7 @@ const EditModeMenu = () => {
|
|||||||
const [isEditMode, setEditMode] = useAtom(editModeAtom);
|
const [isEditMode, setEditMode] = useAtom(editModeAtom);
|
||||||
const board = useRequiredBoard();
|
const board = useRequiredBoard();
|
||||||
const t = useScopedI18n("board.action.edit");
|
const t = useScopedI18n("board.action.edit");
|
||||||
const { mutate, isPending } = clientApi.board.save.useMutation({
|
const { mutate: saveBoard, isPending } = clientApi.board.save.useMutation({
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
showSuccessNotification({
|
showSuccessNotification({
|
||||||
title: t("notification.success.title"),
|
title: t("notification.success.title"),
|
||||||
@@ -125,7 +125,11 @@ const EditModeMenu = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (isEditMode) return mutate(board);
|
if (isEditMode)
|
||||||
|
return saveBoard({
|
||||||
|
boardId: board.id,
|
||||||
|
...board,
|
||||||
|
});
|
||||||
setEditMode(true);
|
setEditMode(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface Props {
|
|||||||
export const GeneralSettingsContent = ({ board }: Props) => {
|
export const GeneralSettingsContent = ({ board }: Props) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { updateBoard } = useUpdateBoard();
|
const { updateBoard } = useUpdateBoard();
|
||||||
const { mutate, isPending } =
|
const { mutate: saveGeneralSettings, isPending } =
|
||||||
clientApi.board.saveGeneralSettings.useMutation();
|
clientApi.board.saveGeneralSettings.useMutation();
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -46,7 +46,10 @@ export const GeneralSettingsContent = ({ board }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit((values) => {
|
onSubmit={form.onSubmit((values) => {
|
||||||
mutate(values);
|
saveGeneralSettings({
|
||||||
|
boardId: board.id,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"clean:workspaces": "turbo clean",
|
"clean:workspaces": "turbo clean",
|
||||||
"db:push": "pnpm -F db push",
|
"db:push": "pnpm -F db push",
|
||||||
"db:studio": "pnpm -F db studio",
|
"db:studio": "pnpm -F db studio",
|
||||||
|
"db:migration:generate": "pnpm -F db migration:generate",
|
||||||
"dev": "node start.js",
|
"dev": "node start.js",
|
||||||
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
||||||
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
|
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
|
||||||
@@ -24,11 +25,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
|
"@testing-library/react-hooks": "^8.0.1",
|
||||||
"@turbo/gen": "^1.12.3",
|
"@turbo/gen": "^1.12.3",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@vitest/coverage-v8": "^1.2.2",
|
"@vitest/coverage-v8": "^1.2.2",
|
||||||
"@vitest/ui": "^1.2.2",
|
"@vitest/ui": "^1.2.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"jsdom": "^24.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"turbo": "^1.12.3",
|
"turbo": "^1.12.3",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import type { Database } from "@homarr/db";
|
import type { Database, SQL } from "@homarr/db";
|
||||||
import { and, createId, db, eq, inArray } from "@homarr/db";
|
import { and, createId, eq, inArray } from "@homarr/db";
|
||||||
import {
|
import {
|
||||||
boards,
|
boards,
|
||||||
integrationItems,
|
integrationItems,
|
||||||
@@ -47,8 +47,8 @@ const filterUpdatedItems = <TInput extends { id: string }>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
getAll: publicProcedure.query(async () => {
|
getAll: publicProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.boards.findMany({
|
return await ctx.db.query.boards.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
@@ -85,23 +85,45 @@ export const boardRouter = createTRPCRouter({
|
|||||||
await ctx.db.delete(boards).where(eq(boards.id, input.id));
|
await ctx.db.delete(boards).where(eq(boards.id, input.id));
|
||||||
}),
|
}),
|
||||||
default: publicProcedure.query(async ({ ctx }) => {
|
default: publicProcedure.query(async ({ ctx }) => {
|
||||||
return await getFullBoardByName(ctx.db, "default");
|
return await getFullBoardWithWhere(ctx.db, eq(boards.name, "default"));
|
||||||
}),
|
}),
|
||||||
byName: publicProcedure
|
byName: publicProcedure
|
||||||
.input(validation.board.byName)
|
.input(validation.board.byName)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
return await getFullBoardByName(ctx.db, input.name);
|
return await getFullBoardWithWhere(ctx.db, eq(boards.name, input.name));
|
||||||
}),
|
}),
|
||||||
saveGeneralSettings: publicProcedure
|
saveGeneralSettings: publicProcedure
|
||||||
.input(validation.board.saveGeneralSettings)
|
.input(validation.board.saveGeneralSettings)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await db.update(boards).set(input).where(eq(boards.name, "default"));
|
const board = await ctx.db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, input.boardId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!board) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Board not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db
|
||||||
|
.update(boards)
|
||||||
|
.set({
|
||||||
|
pageTitle: input.pageTitle,
|
||||||
|
metaTitle: input.metaTitle,
|
||||||
|
logoImageUrl: input.logoImageUrl,
|
||||||
|
faviconImageUrl: input.faviconImageUrl,
|
||||||
|
})
|
||||||
|
.where(eq(boards.id, input.boardId));
|
||||||
}),
|
}),
|
||||||
save: publicProcedure
|
save: publicProcedure
|
||||||
.input(validation.board.save)
|
.input(validation.board.save)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
await ctx.db.transaction(async (tx) => {
|
await ctx.db.transaction(async (tx) => {
|
||||||
const dbBoard = await getFullBoardByName(tx, input.name);
|
const dbBoard = await getFullBoardWithWhere(
|
||||||
|
tx,
|
||||||
|
eq(boards.id, input.boardId),
|
||||||
|
);
|
||||||
|
|
||||||
const addedSections = filterAddedItems(
|
const addedSections = filterAddedItems(
|
||||||
input.sections,
|
input.sections,
|
||||||
@@ -199,12 +221,17 @@ export const boardRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const section of updatedSections) {
|
for (const section of updatedSections) {
|
||||||
|
const prev = dbBoard.sections.find(
|
||||||
|
(dbSection) => dbSection.id === section.id,
|
||||||
|
);
|
||||||
await tx
|
await tx
|
||||||
.update(sections)
|
.update(sections)
|
||||||
.set({
|
.set({
|
||||||
kind: section.kind,
|
|
||||||
position: section.position,
|
position: section.position,
|
||||||
name: "name" in section ? section.name : null,
|
name:
|
||||||
|
prev?.kind === "category" && "name" in section
|
||||||
|
? section.name
|
||||||
|
: null,
|
||||||
})
|
})
|
||||||
.where(eq(sections.id, section.id));
|
.where(eq(sections.id, section.id));
|
||||||
}
|
}
|
||||||
@@ -249,9 +276,9 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getFullBoardByName = async (db: Database, name: string) => {
|
const getFullBoardWithWhere = async (db: Database, where: SQL<unknown>) => {
|
||||||
const board = await db.query.boards.findFirst({
|
const board = await db.query.boards.findFirst({
|
||||||
where: eq(boards.name, name),
|
where,
|
||||||
with: {
|
with: {
|
||||||
sections: {
|
sections: {
|
||||||
with: {
|
with: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
import { and, createId, eq } from "@homarr/db";
|
import { and, createId, eq } from "@homarr/db";
|
||||||
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||||
@@ -128,18 +129,20 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
|
|
||||||
if (changedSecrets.length > 0) {
|
if (changedSecrets.length > 0) {
|
||||||
for (const changedSecret of changedSecrets) {
|
for (const changedSecret of changedSecrets) {
|
||||||
await ctx.db
|
const secretInput = {
|
||||||
.update(integrationSecrets)
|
integrationId: input.id,
|
||||||
.set({
|
value: changedSecret.value,
|
||||||
value: encryptSecret(changedSecret.value),
|
kind: changedSecret.kind,
|
||||||
updatedAt: new Date(),
|
};
|
||||||
})
|
if (
|
||||||
.where(
|
!decryptedSecrets.some(
|
||||||
and(
|
(secret) => secret.kind === changedSecret.kind,
|
||||||
eq(integrationSecrets.integrationId, input.id),
|
)
|
||||||
eq(integrationSecrets.kind, changedSecret.kind),
|
) {
|
||||||
),
|
await addSecret(ctx.db, secretInput);
|
||||||
);
|
} else {
|
||||||
|
await updateSecret(ctx.db, secretInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -204,6 +207,17 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const everySecretDefined = secretKinds.every((secretKind) =>
|
||||||
|
secrets.some((secret) => secret.kind === secretKind),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!everySecretDefined) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "SECRETS_NOT_DEFINED",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: actually test the connection
|
// TODO: actually test the connection
|
||||||
@@ -223,7 +237,7 @@ const key = Buffer.from(
|
|||||||
); // TODO: generate with const data = crypto.randomBytes(32).toString('hex')
|
); // TODO: generate with const data = crypto.randomBytes(32).toString('hex')
|
||||||
|
|
||||||
//Encrypting text
|
//Encrypting text
|
||||||
function encryptSecret(text: string): `${string}.${string}` {
|
export function encryptSecret(text: string): `${string}.${string}` {
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
|
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
|
||||||
let encrypted = cipher.update(text);
|
let encrypted = cipher.update(text);
|
||||||
@@ -241,3 +255,37 @@ function decryptSecret(value: `${string}.${string}`) {
|
|||||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||||
return decrypted.toString();
|
return decrypted.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateSecretInput {
|
||||||
|
integrationId: string;
|
||||||
|
value: string;
|
||||||
|
kind: IntegrationSecretKind;
|
||||||
|
}
|
||||||
|
const updateSecret = async (db: Database, input: UpdateSecretInput) => {
|
||||||
|
await db
|
||||||
|
.update(integrationSecrets)
|
||||||
|
.set({
|
||||||
|
value: encryptSecret(input.value),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(integrationSecrets.integrationId, input.integrationId),
|
||||||
|
eq(integrationSecrets.kind, input.kind),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AddSecretInput {
|
||||||
|
integrationId: string;
|
||||||
|
value: string;
|
||||||
|
kind: IntegrationSecretKind;
|
||||||
|
}
|
||||||
|
const addSecret = async (db: Database, input: AddSecretInput) => {
|
||||||
|
await db.insert(integrationSecrets).values({
|
||||||
|
kind: input.kind,
|
||||||
|
value: encryptSecret(input.value),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
integrationId: input.integrationId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
648
packages/api/src/router/test/board.spec.ts
Normal file
648
packages/api/src/router/test/board.spec.ts
Normal file
@@ -0,0 +1,648 @@
|
|||||||
|
import SuperJSON from "superjson";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Session } from "@homarr/auth";
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { createId, eq } from "@homarr/db";
|
||||||
|
import {
|
||||||
|
boards,
|
||||||
|
integrationItems,
|
||||||
|
integrations,
|
||||||
|
items,
|
||||||
|
sections,
|
||||||
|
} from "@homarr/db/schema/sqlite";
|
||||||
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "../../..";
|
||||||
|
import { boardRouter } from "../board";
|
||||||
|
|
||||||
|
// Mock the auth module to return an empty session
|
||||||
|
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||||
|
|
||||||
|
export const expectToBeDefined = <T>(value: T) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
expect(value).toBeDefined();
|
||||||
|
}
|
||||||
|
if (value === null) {
|
||||||
|
expect(value).not.toBeNull();
|
||||||
|
}
|
||||||
|
return value as Exclude<T, undefined | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("default should return default board", () => {
|
||||||
|
it("should return default board", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const fullBoardProps = await createFullBoardAsync(db, "default");
|
||||||
|
|
||||||
|
const result = await caller.default();
|
||||||
|
|
||||||
|
expectInputToBeFullBoardWithName(result, {
|
||||||
|
name: "default",
|
||||||
|
...fullBoardProps,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("byName should return board by name", () => {
|
||||||
|
it.each([["default"], ["something"]])(
|
||||||
|
"should return board by name %s when present",
|
||||||
|
async (name) => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const fullBoardProps = await createFullBoardAsync(db, name);
|
||||||
|
|
||||||
|
const result = await caller.byName({ name });
|
||||||
|
|
||||||
|
expectInputToBeFullBoardWithName(result, {
|
||||||
|
name,
|
||||||
|
...fullBoardProps,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should throw error when not present");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("saveGeneralSettings should save general settings", () => {
|
||||||
|
it("should save general settings", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const newPageTitle = "newPageTitle";
|
||||||
|
const newMetaTitle = "newMetaTitle";
|
||||||
|
const newLogoImageUrl = "http://logo.image/url.png";
|
||||||
|
const newFaviconImageUrl = "http://favicon.image/url.png";
|
||||||
|
|
||||||
|
const { boardId } = await createFullBoardAsync(db, "default");
|
||||||
|
|
||||||
|
await caller.saveGeneralSettings({
|
||||||
|
pageTitle: newPageTitle,
|
||||||
|
metaTitle: newMetaTitle,
|
||||||
|
logoImageUrl: newLogoImageUrl,
|
||||||
|
faviconImageUrl: newFaviconImageUrl,
|
||||||
|
boardId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when board not found", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.saveGeneralSettings({
|
||||||
|
pageTitle: "newPageTitle",
|
||||||
|
metaTitle: "newMetaTitle",
|
||||||
|
logoImageUrl: "http://logo.image/url.png",
|
||||||
|
faviconImageUrl: "http://favicon.image/url.png",
|
||||||
|
boardId: "nonExistentBoardId",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(act()).rejects.toThrowError("Board not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("save should save full board", () => {
|
||||||
|
it("should remove section when not present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||||
|
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const section = await db.query.boards.findFirst({
|
||||||
|
where: eq(sections.id, sectionId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(1);
|
||||||
|
expect(definedBoard.sections[0]?.id).not.toBe(sectionId);
|
||||||
|
expect(section).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("should remove item when not present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const { boardId, itemId, sectionId } = await createFullBoardAsync(
|
||||||
|
db,
|
||||||
|
"default",
|
||||||
|
);
|
||||||
|
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
kind: "clock",
|
||||||
|
options: { is24HourFormat: true },
|
||||||
|
integrations: [],
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
items: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await db.query.items.findFirst({
|
||||||
|
where: eq(items.id, itemId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(1);
|
||||||
|
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||||
|
expect(firstSection.items.length).toBe(1);
|
||||||
|
expect(firstSection.items[0]?.id).not.toBe(itemId);
|
||||||
|
expect(item).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("should remove integration reference when not present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
const anotherIntegration = {
|
||||||
|
id: createId(),
|
||||||
|
kind: "adGuardHome",
|
||||||
|
name: "AdGuard Home",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const { boardId, itemId, integrationId, sectionId } =
|
||||||
|
await createFullBoardAsync(db, "default");
|
||||||
|
await db.insert(integrations).values(anotherIntegration);
|
||||||
|
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: itemId,
|
||||||
|
kind: "clock",
|
||||||
|
options: { is24HourFormat: true },
|
||||||
|
integrations: [anotherIntegration],
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
items: {
|
||||||
|
with: {
|
||||||
|
integrations: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const integration = await db.query.integrationItems.findFirst({
|
||||||
|
where: eq(integrationItems.integrationId, integrationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(1);
|
||||||
|
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||||
|
expect(firstSection.items.length).toBe(1);
|
||||||
|
const firstItem = expectToBeDefined(firstSection.items[0]);
|
||||||
|
expect(firstItem.integrations.length).toBe(1);
|
||||||
|
expect(firstItem.integrations[0]?.integrationId).not.toBe(integrationId);
|
||||||
|
expect(integration).toBeUndefined();
|
||||||
|
});
|
||||||
|
it.each([
|
||||||
|
[{ kind: "empty" as const }],
|
||||||
|
[{ kind: "category" as const, name: "My first category" }],
|
||||||
|
])("should add section when present in input", async (partialSection) => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||||
|
|
||||||
|
const newSectionId = createId();
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: newSectionId,
|
||||||
|
position: 1,
|
||||||
|
items: [],
|
||||||
|
...partialSection,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const section = await db.query.sections.findFirst({
|
||||||
|
where: eq(sections.id, newSectionId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(2);
|
||||||
|
const addedSection = expectToBeDefined(
|
||||||
|
definedBoard.sections.find((section) => section.id === newSectionId),
|
||||||
|
);
|
||||||
|
expect(addedSection).toBeDefined();
|
||||||
|
expect(addedSection.id).toBe(newSectionId);
|
||||||
|
expect(addedSection.kind).toBe(partialSection.kind);
|
||||||
|
expect(addedSection.position).toBe(1);
|
||||||
|
if ("name" in partialSection) {
|
||||||
|
expect(addedSection.name).toBe(partialSection.name);
|
||||||
|
}
|
||||||
|
expect(section).toBeDefined();
|
||||||
|
});
|
||||||
|
it("should add item when present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||||
|
|
||||||
|
const newItemId = createId();
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: newItemId,
|
||||||
|
kind: "clock",
|
||||||
|
options: { is24HourFormat: true },
|
||||||
|
integrations: [],
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
xOffset: 3,
|
||||||
|
yOffset: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
items: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await db.query.items.findFirst({
|
||||||
|
where: eq(items.id, newItemId),
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(1);
|
||||||
|
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||||
|
expect(firstSection.items.length).toBe(1);
|
||||||
|
const addedItem = expectToBeDefined(
|
||||||
|
firstSection.items.find((item) => item.id === newItemId),
|
||||||
|
);
|
||||||
|
expect(addedItem).toBeDefined();
|
||||||
|
expect(addedItem.id).toBe(newItemId);
|
||||||
|
expect(addedItem.kind).toBe("clock");
|
||||||
|
expect(addedItem.options).toBe(
|
||||||
|
SuperJSON.stringify({ is24HourFormat: true }),
|
||||||
|
);
|
||||||
|
expect(addedItem.height).toBe(1);
|
||||||
|
expect(addedItem.width).toBe(1);
|
||||||
|
expect(addedItem.xOffset).toBe(3);
|
||||||
|
expect(addedItem.yOffset).toBe(2);
|
||||||
|
expect(item).toBeDefined();
|
||||||
|
});
|
||||||
|
it("should add integration reference when present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
const integration = {
|
||||||
|
id: createId(),
|
||||||
|
kind: "plex",
|
||||||
|
name: "Plex",
|
||||||
|
url: "http://plex.local",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const { boardId, itemId, sectionId } = await createFullBoardAsync(
|
||||||
|
db,
|
||||||
|
"default",
|
||||||
|
);
|
||||||
|
await db.insert(integrations).values(integration);
|
||||||
|
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: itemId,
|
||||||
|
kind: "clock",
|
||||||
|
options: { is24HourFormat: true },
|
||||||
|
integrations: [integration],
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
items: {
|
||||||
|
with: {
|
||||||
|
integrations: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationItem = await db.query.integrationItems.findFirst({
|
||||||
|
where: eq(integrationItems.integrationId, integration.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(1);
|
||||||
|
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||||
|
expect(firstSection.items.length).toBe(1);
|
||||||
|
const firstItem = expectToBeDefined(
|
||||||
|
firstSection.items.find((item) => item.id === itemId),
|
||||||
|
);
|
||||||
|
expect(firstItem.integrations.length).toBe(1);
|
||||||
|
expect(firstItem.integrations[0]?.integrationId).toBe(integration.id);
|
||||||
|
expect(integrationItem).toBeDefined();
|
||||||
|
});
|
||||||
|
it("should update section when present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const { boardId, sectionId } = await createFullBoardAsync(db, "default");
|
||||||
|
const newSectionId = createId();
|
||||||
|
await db.insert(sections).values({
|
||||||
|
id: newSectionId,
|
||||||
|
kind: "category",
|
||||||
|
name: "Before",
|
||||||
|
position: 1,
|
||||||
|
boardId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "category",
|
||||||
|
position: 1,
|
||||||
|
name: "Test",
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: newSectionId,
|
||||||
|
kind: "category",
|
||||||
|
name: "After",
|
||||||
|
position: 0,
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(2);
|
||||||
|
const firstSection = expectToBeDefined(
|
||||||
|
definedBoard.sections.find((section) => section.id === sectionId),
|
||||||
|
);
|
||||||
|
expect(firstSection.id).toBe(sectionId);
|
||||||
|
expect(firstSection.kind).toBe("empty");
|
||||||
|
expect(firstSection.position).toBe(1);
|
||||||
|
expect(firstSection.name).toBe(null);
|
||||||
|
const secondSection = expectToBeDefined(
|
||||||
|
definedBoard.sections.find((section) => section.id === newSectionId),
|
||||||
|
);
|
||||||
|
expect(secondSection.id).toBe(newSectionId);
|
||||||
|
expect(secondSection.kind).toBe("category");
|
||||||
|
expect(secondSection.position).toBe(0);
|
||||||
|
expect(secondSection.name).toBe("After");
|
||||||
|
});
|
||||||
|
it("should update item when present in input", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const { boardId, itemId, sectionId } = await createFullBoardAsync(
|
||||||
|
db,
|
||||||
|
"default",
|
||||||
|
);
|
||||||
|
|
||||||
|
await caller.save({
|
||||||
|
boardId,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: itemId,
|
||||||
|
kind: "clock",
|
||||||
|
options: { is24HourFormat: false },
|
||||||
|
integrations: [],
|
||||||
|
height: 3,
|
||||||
|
width: 2,
|
||||||
|
xOffset: 7,
|
||||||
|
yOffset: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const board = await db.query.boards.findFirst({
|
||||||
|
where: eq(boards.id, boardId),
|
||||||
|
with: {
|
||||||
|
sections: {
|
||||||
|
with: {
|
||||||
|
items: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const definedBoard = expectToBeDefined(board);
|
||||||
|
expect(definedBoard.sections.length).toBe(1);
|
||||||
|
const firstSection = expectToBeDefined(definedBoard.sections[0]);
|
||||||
|
expect(firstSection.items.length).toBe(1);
|
||||||
|
const firstItem = expectToBeDefined(
|
||||||
|
firstSection.items.find((item) => item.id === itemId),
|
||||||
|
);
|
||||||
|
expect(firstItem.id).toBe(itemId);
|
||||||
|
expect(firstItem.kind).toBe("clock");
|
||||||
|
expect(
|
||||||
|
SuperJSON.parse<{ is24HourFormat: boolean }>(firstItem.options)
|
||||||
|
.is24HourFormat,
|
||||||
|
).toBe(false);
|
||||||
|
expect(firstItem.height).toBe(3);
|
||||||
|
expect(firstItem.width).toBe(2);
|
||||||
|
expect(firstItem.xOffset).toBe(7);
|
||||||
|
expect(firstItem.yOffset).toBe(5);
|
||||||
|
});
|
||||||
|
it("should fail when board not found", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = boardRouter.createCaller({ db, session: null });
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.save({
|
||||||
|
boardId: "nonExistentBoardId",
|
||||||
|
sections: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(act()).rejects.toThrowError("Board not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectInputToBeFullBoardWithName = (
|
||||||
|
input: RouterOutputs["board"]["default"],
|
||||||
|
props: { name: string } & Awaited<ReturnType<typeof createFullBoardAsync>>,
|
||||||
|
) => {
|
||||||
|
expect(input.id).toBe(props.boardId);
|
||||||
|
expect(input.name).toBe(props.name);
|
||||||
|
expect(input.sections.length).toBe(1);
|
||||||
|
const firstSection = expectToBeDefined(input.sections[0]);
|
||||||
|
expect(firstSection.id).toBe(props.sectionId);
|
||||||
|
expect(firstSection.items.length).toBe(1);
|
||||||
|
const firstItem = expectToBeDefined(firstSection.items[0]);
|
||||||
|
expect(firstItem.id).toBe(props.itemId);
|
||||||
|
expect(firstItem.kind).toBe("clock");
|
||||||
|
if (firstItem.kind === "clock") {
|
||||||
|
expect(firstItem.options.is24HourFormat).toBe(true);
|
||||||
|
}
|
||||||
|
expect(firstItem.integrations.length).toBe(1);
|
||||||
|
const firstIntegration = expectToBeDefined(firstItem.integrations[0]);
|
||||||
|
expect(firstIntegration.id).toBe(props.integrationId);
|
||||||
|
expect(firstIntegration.kind).toBe("adGuardHome");
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFullBoardAsync = async (db: Database, name: string) => {
|
||||||
|
const boardId = createId();
|
||||||
|
await db.insert(boards).values({
|
||||||
|
id: boardId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sectionId = createId();
|
||||||
|
await db.insert(sections).values({
|
||||||
|
id: sectionId,
|
||||||
|
kind: "empty",
|
||||||
|
position: 0,
|
||||||
|
boardId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemId = createId();
|
||||||
|
await db.insert(items).values({
|
||||||
|
id: itemId,
|
||||||
|
kind: "clock",
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
xOffset: 0,
|
||||||
|
yOffset: 0,
|
||||||
|
sectionId,
|
||||||
|
options: SuperJSON.stringify({ is24HourFormat: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationId = createId();
|
||||||
|
await db.insert(integrations).values({
|
||||||
|
id: integrationId,
|
||||||
|
kind: "adGuardHome",
|
||||||
|
name: "AdGuard Home",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrationItems).values({
|
||||||
|
integrationId,
|
||||||
|
itemId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
boardId,
|
||||||
|
sectionId,
|
||||||
|
itemId,
|
||||||
|
integrationId,
|
||||||
|
};
|
||||||
|
};
|
||||||
503
packages/api/src/router/test/integration.spec.ts
Normal file
503
packages/api/src/router/test/integration.spec.ts
Normal file
@@ -0,0 +1,503 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Session } from "@homarr/auth";
|
||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite";
|
||||||
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
|
import type { RouterInputs } from "../../..";
|
||||||
|
import { encryptSecret, integrationRouter } from "../integration";
|
||||||
|
import { expectToBeDefined } from "./board.spec";
|
||||||
|
|
||||||
|
// Mock the auth module to return an empty session
|
||||||
|
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||||
|
|
||||||
|
describe("all should return all integrations", () => {
|
||||||
|
it("should return all integrations", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrations).values([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Home assistant",
|
||||||
|
kind: "homeAssistant",
|
||||||
|
url: "http://homeassist.local",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Home plex server",
|
||||||
|
kind: "plex",
|
||||||
|
url: "http://plex.local",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await caller.all();
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
expect(result[0]!.kind).toBe("plex");
|
||||||
|
expect(result[1]!.kind).toBe("homeAssistant");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("byId should return an integration by id", () => {
|
||||||
|
it("should return an integration by id", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrations).values([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Home assistant",
|
||||||
|
kind: "homeAssistant",
|
||||||
|
url: "http://homeassist.local",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Home plex server",
|
||||||
|
kind: "plex",
|
||||||
|
url: "http://plex.local",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await caller.byId({ id: "2" });
|
||||||
|
expect(result.kind).toBe("plex");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if the integration does not exist", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const act = async () => await caller.byId({ id: "2" });
|
||||||
|
await expect(act()).rejects.toThrow("Integration not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should only return the public secret values", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrations).values([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Home assistant",
|
||||||
|
kind: "homeAssistant",
|
||||||
|
url: "http://homeassist.local",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await db.insert(integrationSecrets).values([
|
||||||
|
{
|
||||||
|
kind: "username",
|
||||||
|
value: encryptSecret("musterUser"),
|
||||||
|
integrationId: "1",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "password",
|
||||||
|
value: encryptSecret("Password123!"),
|
||||||
|
integrationId: "1",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "apiKey",
|
||||||
|
value: encryptSecret("1234567890"),
|
||||||
|
integrationId: "1",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await caller.byId({ id: "1" });
|
||||||
|
expect(result.secrets.length).toBe(3);
|
||||||
|
const username = expectToBeDefined(
|
||||||
|
result.secrets.find((secret) => secret.kind === "username"),
|
||||||
|
);
|
||||||
|
expect(username.value).not.toBeNull();
|
||||||
|
const password = expectToBeDefined(
|
||||||
|
result.secrets.find((secret) => secret.kind === "password"),
|
||||||
|
);
|
||||||
|
expect(password.value).toBeNull();
|
||||||
|
const apiKey = expectToBeDefined(
|
||||||
|
result.secrets.find((secret) => secret.kind === "apiKey"),
|
||||||
|
);
|
||||||
|
expect(apiKey.value).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("create should create a new integration", () => {
|
||||||
|
it("should create a new integration", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
const input = {
|
||||||
|
name: "Jellyfin",
|
||||||
|
kind: "jellyfin" as const,
|
||||||
|
url: "http://jellyfin.local",
|
||||||
|
secrets: [{ kind: "apiKey" as const, value: "1234567890" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(fakeNow);
|
||||||
|
await caller.create(input);
|
||||||
|
vi.useRealTimers();
|
||||||
|
|
||||||
|
const dbIntegration = await db.query.integrations.findFirst();
|
||||||
|
const dbSecret = await db.query.integrationSecrets.findFirst();
|
||||||
|
expect(dbIntegration).toBeDefined();
|
||||||
|
expect(dbIntegration!.name).toBe(input.name);
|
||||||
|
expect(dbIntegration!.kind).toBe(input.kind);
|
||||||
|
expect(dbIntegration!.url).toBe(input.url);
|
||||||
|
|
||||||
|
expect(dbSecret!.integrationId).toBe(dbIntegration!.id);
|
||||||
|
expect(dbSecret).toBeDefined();
|
||||||
|
expect(dbSecret!.kind).toBe(input.secrets[0]!.kind);
|
||||||
|
expect(dbSecret!.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||||
|
expect(dbSecret!.updatedAt).toEqual(fakeNow);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("update should update an integration", () => {
|
||||||
|
it("should update an integration", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastWeek = new Date("2023-06-24T00:00:00Z");
|
||||||
|
const integrationId = createId();
|
||||||
|
const toInsert = {
|
||||||
|
id: integrationId,
|
||||||
|
name: "Pi Hole",
|
||||||
|
kind: "piHole" as const,
|
||||||
|
url: "http://hole.local",
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.insert(integrations).values(toInsert);
|
||||||
|
|
||||||
|
const usernameToInsert = {
|
||||||
|
kind: "username" as const,
|
||||||
|
value: encryptSecret("musterUser"),
|
||||||
|
integrationId,
|
||||||
|
updatedAt: lastWeek,
|
||||||
|
};
|
||||||
|
|
||||||
|
const passwordToInsert = {
|
||||||
|
kind: "password" as const,
|
||||||
|
value: encryptSecret("Password123!"),
|
||||||
|
integrationId,
|
||||||
|
updatedAt: lastWeek,
|
||||||
|
};
|
||||||
|
await db
|
||||||
|
.insert(integrationSecrets)
|
||||||
|
.values([usernameToInsert, passwordToInsert]);
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
id: integrationId,
|
||||||
|
name: "Milky Way Pi Hole",
|
||||||
|
kind: "piHole" as const,
|
||||||
|
url: "http://milkyway.local",
|
||||||
|
secrets: [
|
||||||
|
{ kind: "username" as const, value: "newUser" },
|
||||||
|
{ kind: "password" as const, value: null },
|
||||||
|
{ kind: "apiKey" as const, value: "1234567890" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fakeNow = new Date("2023-07-01T00:00:00Z");
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(fakeNow);
|
||||||
|
await caller.update(input);
|
||||||
|
vi.useRealTimers();
|
||||||
|
|
||||||
|
const dbIntegration = await db.query.integrations.findFirst();
|
||||||
|
const dbSecrets = await db.query.integrationSecrets.findMany();
|
||||||
|
|
||||||
|
expect(dbIntegration).toBeDefined();
|
||||||
|
expect(dbIntegration!.name).toBe(input.name);
|
||||||
|
expect(dbIntegration!.kind).toBe(input.kind);
|
||||||
|
expect(dbIntegration!.url).toBe(input.url);
|
||||||
|
|
||||||
|
expect(dbSecrets.length).toBe(3);
|
||||||
|
const username = expectToBeDefined(
|
||||||
|
dbSecrets.find((secret) => secret.kind === "username"),
|
||||||
|
);
|
||||||
|
const password = expectToBeDefined(
|
||||||
|
dbSecrets.find((secret) => secret.kind === "password"),
|
||||||
|
);
|
||||||
|
const apiKey = expectToBeDefined(
|
||||||
|
dbSecrets.find((secret) => secret.kind === "apiKey"),
|
||||||
|
);
|
||||||
|
expect(username.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||||
|
expect(password.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||||
|
expect(apiKey.value).toMatch(/^[a-f0-9]+.[a-f0-9]+$/);
|
||||||
|
expect(username.updatedAt).toEqual(fakeNow);
|
||||||
|
expect(password.updatedAt).toEqual(lastWeek);
|
||||||
|
expect(apiKey.updatedAt).toEqual(fakeNow);
|
||||||
|
expect(username.value).not.toEqual(usernameToInsert.value);
|
||||||
|
expect(password.value).toEqual(passwordToInsert.value);
|
||||||
|
expect(apiKey.value).not.toEqual(input.secrets[2]!.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw an error if the integration does not exist", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.update({
|
||||||
|
id: createId(),
|
||||||
|
name: "Pi Hole",
|
||||||
|
url: "http://hole.local",
|
||||||
|
secrets: [],
|
||||||
|
});
|
||||||
|
await expect(act()).rejects.toThrow("Integration not found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("delete should delete an integration", () => {
|
||||||
|
it("should delete an integration", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationId = createId();
|
||||||
|
await db.insert(integrations).values({
|
||||||
|
id: integrationId,
|
||||||
|
name: "Home assistant",
|
||||||
|
kind: "homeAssistant",
|
||||||
|
url: "http://homeassist.local",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrationSecrets).values([
|
||||||
|
{
|
||||||
|
kind: "username",
|
||||||
|
value: encryptSecret("example"),
|
||||||
|
integrationId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await caller.delete({ id: integrationId });
|
||||||
|
|
||||||
|
const dbIntegration = await db.query.integrations.findFirst();
|
||||||
|
expect(dbIntegration).toBeUndefined();
|
||||||
|
const dbSecrets = await db.query.integrationSecrets.findMany();
|
||||||
|
expect(dbSecrets.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("testConnection should test the connection to an integration", () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
"nzbGet" as const,
|
||||||
|
[
|
||||||
|
{ kind: "username" as const, value: null },
|
||||||
|
{ kind: "password" as const, value: "Password123!" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"nzbGet" as const,
|
||||||
|
[
|
||||||
|
{ kind: "username" as const, value: "exampleUser" },
|
||||||
|
{ kind: "password" as const, value: null },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
["sabNzbd" as const, [{ kind: "apiKey" as const, value: null }]],
|
||||||
|
[
|
||||||
|
"sabNzbd" as const,
|
||||||
|
[
|
||||||
|
{ kind: "username" as const, value: "exampleUser" },
|
||||||
|
{ kind: "password" as const, value: "Password123!" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])(
|
||||||
|
"should fail when a required secret is missing when creating %s integration",
|
||||||
|
async (kind, secrets) => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input: RouterInputs["integration"]["testConnection"] = {
|
||||||
|
id: null,
|
||||||
|
kind,
|
||||||
|
url: `http://${kind}.local`,
|
||||||
|
secrets,
|
||||||
|
};
|
||||||
|
|
||||||
|
const act = async () => await caller.testConnection(input);
|
||||||
|
await expect(act()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
"nzbGet" as const,
|
||||||
|
[
|
||||||
|
{ kind: "username" as const, value: "exampleUser" },
|
||||||
|
{ kind: "password" as const, value: "Password123!" },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
["sabNzbd" as const, [{ kind: "apiKey" as const, value: "1234567890" }]],
|
||||||
|
])(
|
||||||
|
"should be successful when all required secrets are defined for creation of %s integration",
|
||||||
|
async (kind, secrets) => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input: RouterInputs["integration"]["testConnection"] = {
|
||||||
|
id: null,
|
||||||
|
kind,
|
||||||
|
url: `http://${kind}.local`,
|
||||||
|
secrets,
|
||||||
|
};
|
||||||
|
|
||||||
|
const act = async () => await caller.testConnection(input);
|
||||||
|
await expect(act()).resolves.toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should be successful when all required secrets are defined for updating an nzbGet integration", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input: RouterInputs["integration"]["testConnection"] = {
|
||||||
|
id: createId(),
|
||||||
|
kind: "nzbGet",
|
||||||
|
url: "http://nzbGet.local",
|
||||||
|
secrets: [
|
||||||
|
{ kind: "username", value: "exampleUser" },
|
||||||
|
{ kind: "password", value: "Password123!" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const act = async () => await caller.testConnection(input);
|
||||||
|
await expect(act()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be successful when overriding one of the secrets for an existing nzbGet integration", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationId = createId();
|
||||||
|
await db.insert(integrations).values({
|
||||||
|
id: integrationId,
|
||||||
|
name: "NZBGet",
|
||||||
|
kind: "nzbGet",
|
||||||
|
url: "http://nzbGet.local",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrationSecrets).values([
|
||||||
|
{
|
||||||
|
kind: "username",
|
||||||
|
value: encryptSecret("exampleUser"),
|
||||||
|
integrationId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "password",
|
||||||
|
value: encryptSecret("Password123!"),
|
||||||
|
integrationId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const input: RouterInputs["integration"]["testConnection"] = {
|
||||||
|
id: integrationId,
|
||||||
|
kind: "nzbGet",
|
||||||
|
url: "http://nzbGet.local",
|
||||||
|
secrets: [
|
||||||
|
{ kind: "username", value: "newUser" },
|
||||||
|
{ kind: "password", value: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const act = async () => await caller.testConnection(input);
|
||||||
|
await expect(act()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when a required secret is missing for an existing nzbGet integration", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationId = createId();
|
||||||
|
await db.insert(integrations).values({
|
||||||
|
id: integrationId,
|
||||||
|
name: "NZBGet",
|
||||||
|
kind: "nzbGet",
|
||||||
|
url: "http://nzbGet.local",
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(integrationSecrets).values([
|
||||||
|
{
|
||||||
|
kind: "username",
|
||||||
|
value: encryptSecret("exampleUser"),
|
||||||
|
integrationId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const input: RouterInputs["integration"]["testConnection"] = {
|
||||||
|
id: integrationId,
|
||||||
|
kind: "nzbGet",
|
||||||
|
url: "http://nzbGet.local",
|
||||||
|
secrets: [
|
||||||
|
{ kind: "username", value: "newUser" },
|
||||||
|
{ kind: "apiKey", value: "1234567890" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const act = async () => await caller.testConnection(input);
|
||||||
|
await expect(act()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail when the updating integration does not exist", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = integrationRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.testConnection({
|
||||||
|
id: createId(),
|
||||||
|
kind: "nzbGet",
|
||||||
|
url: "http://nzbGet.local",
|
||||||
|
secrets: [
|
||||||
|
{ kind: "username", value: null },
|
||||||
|
{ kind: "password", value: "Password123!" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await expect(act()).rejects.toThrow("SECRETS_NOT_DEFINED");
|
||||||
|
});
|
||||||
|
});
|
||||||
94
packages/api/src/router/test/user.spec.ts
Normal file
94
packages/api/src/router/test/user.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { Session } from "@homarr/auth";
|
||||||
|
import { schema } from "@homarr/db";
|
||||||
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
|
import { userRouter } from "../user";
|
||||||
|
|
||||||
|
// Mock the auth module to return an empty session
|
||||||
|
vi.mock("@homarr/auth", async () => {
|
||||||
|
const mod = await import("@homarr/auth/security");
|
||||||
|
return { ...mod, auth: () => ({}) as Session };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("initUser should initialize the first user", () => {
|
||||||
|
it("should throw an error if a user already exists", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = userRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.insert(schema.users).values({
|
||||||
|
id: "test",
|
||||||
|
name: "test",
|
||||||
|
password: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.initUser({
|
||||||
|
username: "test",
|
||||||
|
password: "12345678",
|
||||||
|
confirmPassword: "12345678",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(act()).rejects.toThrow("User already exists");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a user if none exists", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = userRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
await caller.initUser({
|
||||||
|
username: "test",
|
||||||
|
password: "12345678",
|
||||||
|
confirmPassword: "12345678",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
columns: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a user if the password and confirmPassword do not match", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = userRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.initUser({
|
||||||
|
username: "test",
|
||||||
|
password: "12345678",
|
||||||
|
confirmPassword: "12345679",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(act()).rejects.toThrow("Passwords do not match");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a user if the password is too short", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const caller = userRouter.createCaller({
|
||||||
|
db,
|
||||||
|
session: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const act = async () =>
|
||||||
|
await caller.initUser({
|
||||||
|
username: "test",
|
||||||
|
password: "1234567",
|
||||||
|
confirmPassword: "1234567",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(act()).rejects.toThrow("too_small");
|
||||||
|
});
|
||||||
|
});
|
||||||
61
packages/auth/callbacks.ts
Normal file
61
packages/auth/callbacks.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
|
import type { Adapter } from "@auth/core/adapters";
|
||||||
|
import type { NextAuthConfig } from "next-auth";
|
||||||
|
|
||||||
|
import {
|
||||||
|
expireDateAfter,
|
||||||
|
generateSessionToken,
|
||||||
|
sessionMaxAgeInSeconds,
|
||||||
|
sessionTokenCookieName,
|
||||||
|
} from "./session";
|
||||||
|
|
||||||
|
export const sessionCallback: NextAuthCallbackOf<"session"> = ({
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
}) => ({
|
||||||
|
...session,
|
||||||
|
user: {
|
||||||
|
...session.user,
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSignInCallback =
|
||||||
|
(
|
||||||
|
adapter: Adapter,
|
||||||
|
isCredentialsRequest: boolean,
|
||||||
|
): NextAuthCallbackOf<"signIn"> =>
|
||||||
|
async ({ user }) => {
|
||||||
|
if (!isCredentialsRequest) return true;
|
||||||
|
|
||||||
|
if (!user) return true;
|
||||||
|
|
||||||
|
// https://github.com/nextauthjs/next-auth/issues/6106
|
||||||
|
if (!adapter?.createSession) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionToken = generateSessionToken();
|
||||||
|
const sessionExpiry = expireDateAfter(sessionMaxAgeInSeconds);
|
||||||
|
|
||||||
|
await adapter.createSession({
|
||||||
|
sessionToken,
|
||||||
|
userId: user.id!,
|
||||||
|
expires: sessionExpiry,
|
||||||
|
});
|
||||||
|
|
||||||
|
cookies().set(sessionTokenCookieName, sessionToken, {
|
||||||
|
path: "/",
|
||||||
|
expires: sessionExpiry,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NextAuthCallbackRecord = Exclude<NextAuthConfig["callbacks"], undefined>;
|
||||||
|
export type NextAuthCallbackOf<TKey extends keyof NextAuthCallbackRecord> =
|
||||||
|
Exclude<NextAuthCallbackRecord[TKey], undefined>;
|
||||||
@@ -5,55 +5,23 @@ import Credentials from "next-auth/providers/credentials";
|
|||||||
|
|
||||||
import { db } from "@homarr/db";
|
import { db } from "@homarr/db";
|
||||||
|
|
||||||
import { credentialsConfiguration } from "./providers/credentials";
|
import { createSignInCallback, sessionCallback } from "./callbacks";
|
||||||
|
import { createCredentialsConfiguration } from "./providers/credentials";
|
||||||
import { EmptyNextAuthProvider } from "./providers/empty";
|
import { EmptyNextAuthProvider } from "./providers/empty";
|
||||||
import { expireDateAfter, generateSessionToken } from "./session";
|
import { sessionMaxAgeInSeconds, sessionTokenCookieName } from "./session";
|
||||||
|
|
||||||
const adapter = DrizzleAdapter(db);
|
const adapter = DrizzleAdapter(db);
|
||||||
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
|
||||||
|
|
||||||
export const createConfiguration = (isCredentialsRequest: boolean) =>
|
export const createConfiguration = (isCredentialsRequest: boolean) =>
|
||||||
NextAuth({
|
NextAuth({
|
||||||
adapter,
|
adapter,
|
||||||
providers: [Credentials(credentialsConfiguration), EmptyNextAuthProvider()],
|
providers: [
|
||||||
|
Credentials(createCredentialsConfiguration(db)),
|
||||||
|
EmptyNextAuthProvider(),
|
||||||
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
session: ({ session, user }) => ({
|
session: sessionCallback,
|
||||||
...session,
|
signIn: createSignInCallback(adapter, isCredentialsRequest),
|
||||||
user: {
|
|
||||||
...session.user,
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
signIn: async ({ user }) => {
|
|
||||||
if (!isCredentialsRequest) return true;
|
|
||||||
|
|
||||||
if (!user) return true;
|
|
||||||
|
|
||||||
const sessionToken = generateSessionToken();
|
|
||||||
const sessionExpiry = expireDateAfter(sessionMaxAgeInSeconds);
|
|
||||||
|
|
||||||
// https://github.com/nextauthjs/next-auth/issues/6106
|
|
||||||
if (!adapter?.createSession) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await adapter.createSession({
|
|
||||||
sessionToken: sessionToken,
|
|
||||||
userId: user.id!,
|
|
||||||
expires: sessionExpiry,
|
|
||||||
});
|
|
||||||
|
|
||||||
cookies().set("next-auth.session-token", sessionToken, {
|
|
||||||
path: "/",
|
|
||||||
expires: sessionExpiry,
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "lax",
|
|
||||||
secure: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
strategy: "database",
|
strategy: "database",
|
||||||
@@ -65,7 +33,7 @@ export const createConfiguration = (isCredentialsRequest: boolean) =>
|
|||||||
},
|
},
|
||||||
jwt: {
|
jwt: {
|
||||||
encode() {
|
encode() {
|
||||||
const cookie = cookies().get("next-auth.session-token")?.value;
|
const cookie = cookies().get(sessionTokenCookieName)?.value;
|
||||||
return cookie ?? "";
|
return cookie ?? "";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@homarr/auth",
|
"name": "@homarr/auth",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./security": "./security.ts",
|
||||||
|
"./client": "./client.ts",
|
||||||
|
"./env.mjs": "./env.mjs"
|
||||||
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
"types": "./index.ts",
|
"types": "./index.ts",
|
||||||
|
|||||||
@@ -1,49 +1,56 @@
|
|||||||
import type Credentials from "@auth/core/providers/credentials";
|
import type Credentials from "@auth/core/providers/credentials";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
import { db, eq } from "@homarr/db";
|
import type { Database } from "@homarr/db";
|
||||||
|
import { eq } from "@homarr/db";
|
||||||
import { users } from "@homarr/db/schema/sqlite";
|
import { users } from "@homarr/db/schema/sqlite";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
type CredentialsConfiguration = Parameters<typeof Credentials>[0];
|
type CredentialsConfiguration = Parameters<typeof Credentials>[0];
|
||||||
|
|
||||||
export const credentialsConfiguration = {
|
export const createCredentialsConfiguration = (db: Database) =>
|
||||||
type: "credentials",
|
({
|
||||||
name: "Credentials",
|
type: "credentials",
|
||||||
credentials: {
|
name: "Credentials",
|
||||||
name: {
|
credentials: {
|
||||||
label: "Username",
|
name: {
|
||||||
type: "text",
|
label: "Username",
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
label: "Password",
|
||||||
|
type: "password",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
password: {
|
async authorize(credentials) {
|
||||||
label: "Password",
|
const data = await validation.user.signIn.parseAsync(credentials);
|
||||||
type: "password",
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.name, data.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.password) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`user ${user.name} is trying to log in. checking password...`,
|
||||||
|
);
|
||||||
|
const isValidPassword = await bcrypt.compare(
|
||||||
|
data.password,
|
||||||
|
user.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
console.log(`password for user ${user.name} was incorrect`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`user ${user.name} successfully authorized`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
}) satisfies CredentialsConfiguration;
|
||||||
async authorize(credentials) {
|
|
||||||
const data = await validation.user.signIn.parseAsync(credentials);
|
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.name, data.name),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user?.password) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`user ${user.name} is trying to log in. checking password...`);
|
|
||||||
const isValidPassword = await bcrypt.compare(data.password, user.password);
|
|
||||||
|
|
||||||
if (!isValidPassword) {
|
|
||||||
console.log(`password for user ${user.name} was incorrect`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`user ${user.name} successfully authorized`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
} satisfies CredentialsConfiguration;
|
|
||||||
|
|||||||
66
packages/auth/providers/test/credentials.spec.ts
Normal file
66
packages/auth/providers/test/credentials.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { users } from "@homarr/db/schema/sqlite";
|
||||||
|
import { createDb } from "@homarr/db/test";
|
||||||
|
|
||||||
|
import { createSalt, hashPassword } from "../../security";
|
||||||
|
import { createCredentialsConfiguration } from "../credentials";
|
||||||
|
|
||||||
|
describe("Credentials authorization", () => {
|
||||||
|
it("should authorize user with correct credentials", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const userId = createId();
|
||||||
|
const salt = await createSalt();
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
name: "test",
|
||||||
|
password: await hashPassword("test", salt),
|
||||||
|
salt,
|
||||||
|
});
|
||||||
|
const result = await createCredentialsConfiguration(db).authorize({
|
||||||
|
name: "test",
|
||||||
|
password: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ id: userId, name: "test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordsThatShouldNotAuthorize = [
|
||||||
|
"wrong",
|
||||||
|
"Test",
|
||||||
|
"test ",
|
||||||
|
" test",
|
||||||
|
" test ",
|
||||||
|
];
|
||||||
|
|
||||||
|
passwordsThatShouldNotAuthorize.forEach((password) => {
|
||||||
|
it(`should not authorize user with incorrect credentials (${password})`, async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const userId = createId();
|
||||||
|
const salt = await createSalt();
|
||||||
|
await db.insert(users).values({
|
||||||
|
id: userId,
|
||||||
|
name: "test",
|
||||||
|
password: await hashPassword("test", salt),
|
||||||
|
salt,
|
||||||
|
});
|
||||||
|
const result = await createCredentialsConfiguration(db).authorize({
|
||||||
|
name: "test",
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not authorize user for not existing user", async () => {
|
||||||
|
const db = createDb();
|
||||||
|
const result = await createCredentialsConfiguration(db).authorize({
|
||||||
|
name: "test",
|
||||||
|
password: "test",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
export const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||||
|
export const sessionTokenCookieName = "next-auth.session-token";
|
||||||
|
|
||||||
export const expireDateAfter = (seconds: number) => {
|
export const expireDateAfter = (seconds: number) => {
|
||||||
return new Date(Date.now() + seconds * 1000);
|
return new Date(Date.now() + seconds * 1000);
|
||||||
};
|
};
|
||||||
|
|||||||
153
packages/auth/test/callbacks.spec.ts
Normal file
153
packages/auth/test/callbacks.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import type { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies";
|
||||||
|
import type { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import type { Adapter, AdapterUser } from "@auth/core/adapters";
|
||||||
|
import type { Account, User } from "next-auth";
|
||||||
|
import type { JWT } from "next-auth/jwt";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createSignInCallback, sessionCallback } from "../callbacks";
|
||||||
|
|
||||||
|
describe("session callback", () => {
|
||||||
|
it("should add id and name to session user", async () => {
|
||||||
|
const user: AdapterUser = {
|
||||||
|
id: "id",
|
||||||
|
name: "name",
|
||||||
|
email: "email",
|
||||||
|
emailVerified: new Date("2023-01-13"),
|
||||||
|
};
|
||||||
|
const token: JWT = {};
|
||||||
|
const result = await sessionCallback({
|
||||||
|
session: {
|
||||||
|
user: {
|
||||||
|
id: "no-id",
|
||||||
|
email: "no-email",
|
||||||
|
emailVerified: new Date("2023-01-13"),
|
||||||
|
},
|
||||||
|
expires: "2023-01-13" as Date & string,
|
||||||
|
sessionToken: "token",
|
||||||
|
userId: "no-id",
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
trigger: "update",
|
||||||
|
newSession: {},
|
||||||
|
});
|
||||||
|
expect(result.user).toBeDefined();
|
||||||
|
expect(result.user!.id).toEqual(user.id);
|
||||||
|
expect(result.user!.name).toEqual(user.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
type AdapterSessionInput = Parameters<
|
||||||
|
Exclude<Adapter["createSession"], undefined>
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
const createAdapter = () => {
|
||||||
|
const result = {
|
||||||
|
createSession: (input: AdapterSessionInput) => input,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.spyOn(result, "createSession");
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
|
type SessionExport = typeof import("../session");
|
||||||
|
const mockSessionToken = "e9ef3010-6981-4a81-b9d6-8495d09cf3b5" as const;
|
||||||
|
const mockSessionExpiry = new Date("2023-07-01");
|
||||||
|
vi.mock("../session", async (importOriginal) => {
|
||||||
|
const mod = await importOriginal<SessionExport>();
|
||||||
|
|
||||||
|
const generateSessionToken = () => mockSessionToken;
|
||||||
|
const expireDateAfter = (_seconds: number) => mockSessionExpiry;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mod,
|
||||||
|
generateSessionToken,
|
||||||
|
expireDateAfter,
|
||||||
|
} satisfies SessionExport;
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||||
|
type HeadersExport = typeof import("next/headers");
|
||||||
|
vi.mock("next/headers", async (importOriginal) => {
|
||||||
|
const mod = await importOriginal<HeadersExport>();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
set: (name: string, value: string, options: Partial<ResponseCookie>) =>
|
||||||
|
options as ResponseCookie,
|
||||||
|
} as unknown as ReadonlyRequestCookies;
|
||||||
|
|
||||||
|
vi.spyOn(result, "set");
|
||||||
|
|
||||||
|
const cookies = () => result;
|
||||||
|
|
||||||
|
return { ...mod, cookies } satisfies HeadersExport;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createSignInCallback", () => {
|
||||||
|
it("should return true if not credentials request", async () => {
|
||||||
|
const isCredentialsRequest = false;
|
||||||
|
const signInCallback = createSignInCallback(
|
||||||
|
createAdapter(),
|
||||||
|
isCredentialsRequest,
|
||||||
|
);
|
||||||
|
const result = await signInCallback({
|
||||||
|
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
||||||
|
account: {} as Account,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if no user", async () => {
|
||||||
|
const isCredentialsRequest = true;
|
||||||
|
const signInCallback = createSignInCallback(
|
||||||
|
createAdapter(),
|
||||||
|
isCredentialsRequest,
|
||||||
|
);
|
||||||
|
const result = await signInCallback({
|
||||||
|
user: undefined as unknown as User,
|
||||||
|
account: {} as Account,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if no adapter.createSession", async () => {
|
||||||
|
const isCredentialsRequest = true;
|
||||||
|
const signInCallback = createSignInCallback(
|
||||||
|
// https://github.com/nextauthjs/next-auth/issues/6106
|
||||||
|
undefined as unknown as Adapter,
|
||||||
|
isCredentialsRequest,
|
||||||
|
);
|
||||||
|
const result = await signInCallback({
|
||||||
|
user: { id: "1", emailVerified: new Date("2023-01-13") },
|
||||||
|
account: {} as Account,
|
||||||
|
});
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call adapter.createSession with correct input", async () => {
|
||||||
|
const adapter = createAdapter();
|
||||||
|
const isCredentialsRequest = true;
|
||||||
|
const signInCallback = createSignInCallback(adapter, isCredentialsRequest);
|
||||||
|
const user = { id: "1", emailVerified: new Date("2023-01-13") };
|
||||||
|
const account = {} as Account;
|
||||||
|
await signInCallback({ user, account });
|
||||||
|
expect(adapter.createSession).toHaveBeenCalledWith({
|
||||||
|
sessionToken: mockSessionToken,
|
||||||
|
userId: user.id,
|
||||||
|
expires: mockSessionExpiry,
|
||||||
|
});
|
||||||
|
expect(cookies().set).toHaveBeenCalledWith(
|
||||||
|
"next-auth.session-token",
|
||||||
|
mockSessionToken,
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
expires: mockSessionExpiry,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
47
packages/auth/test/security.spec.ts
Normal file
47
packages/auth/test/security.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createSalt, hashPassword } from "../security";
|
||||||
|
|
||||||
|
describe("createSalt should return a salt", () => {
|
||||||
|
it("should return a salt", async () => {
|
||||||
|
const result = await createSalt();
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.length).toBeGreaterThan(25);
|
||||||
|
});
|
||||||
|
it("should return a different salt each time", async () => {
|
||||||
|
const result1 = await createSalt();
|
||||||
|
const result2 = await createSalt();
|
||||||
|
expect(result1).not.toEqual(result2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hashPassword should return a hash", () => {
|
||||||
|
it("should return a hash", async () => {
|
||||||
|
const password = "password";
|
||||||
|
const salt = await createSalt();
|
||||||
|
const result = await hashPassword(password, salt);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.length).toBeGreaterThan(55);
|
||||||
|
expect(result).not.toEqual(password);
|
||||||
|
});
|
||||||
|
it("should return a different hash each time", async () => {
|
||||||
|
const password = "password";
|
||||||
|
const password2 = "another password";
|
||||||
|
const salt = await createSalt();
|
||||||
|
|
||||||
|
const result1 = await hashPassword(password, salt);
|
||||||
|
const result2 = await hashPassword(password2, salt);
|
||||||
|
|
||||||
|
expect(result1).not.toEqual(result2);
|
||||||
|
});
|
||||||
|
it("should return a different hash for the same password with different salts", async () => {
|
||||||
|
const password = "password";
|
||||||
|
const salt1 = await createSalt();
|
||||||
|
const salt2 = await createSalt();
|
||||||
|
|
||||||
|
const result1 = await hashPassword(password, salt1);
|
||||||
|
const result2 = await hashPassword(password, salt2);
|
||||||
|
|
||||||
|
expect(result1).not.toEqual(result2);
|
||||||
|
});
|
||||||
|
});
|
||||||
43
packages/auth/test/session.spec.ts
Normal file
43
packages/auth/test/session.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
|
import { expireDateAfter, generateSessionToken } from "../session";
|
||||||
|
|
||||||
|
describe("expireDateAfter should calculate date after specified seconds", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["2023-07-01T00:00:00Z", 60, "2023-07-01T00:01:00Z"], // 1 minute
|
||||||
|
["2023-07-01T00:00:00Z", 60 * 60, "2023-07-01T01:00:00Z"], // 1 hour
|
||||||
|
["2023-07-01T00:00:00Z", 60 * 60 * 24, "2023-07-02T00:00:00Z"], // 1 day
|
||||||
|
["2023-07-01T00:00:00Z", 60 * 60 * 24 * 30, "2023-07-31T00:00:00Z"], // 30 days
|
||||||
|
["2023-07-01T00:00:00Z", 60 * 60 * 24 * 365, "2024-06-30T00:00:00Z"], // 1 year
|
||||||
|
["2023-07-01T00:00:00Z", 60 * 60 * 24 * 365 * 10, "2033-06-28T00:00:00Z"], // 10 years
|
||||||
|
])(
|
||||||
|
"should calculate date %s and after %i seconds to equal %s",
|
||||||
|
(initialDate, seconds, expectedDate) => {
|
||||||
|
vi.setSystemTime(new Date(initialDate));
|
||||||
|
const result = expireDateAfter(seconds);
|
||||||
|
expect(result).toEqual(new Date(expectedDate));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateSessionToken should return a random UUID", () => {
|
||||||
|
it("should return a random UUID", () => {
|
||||||
|
const result = generateSessionToken();
|
||||||
|
expect(z.string().uuid().safeParse(result).success).toBe(true);
|
||||||
|
});
|
||||||
|
it("should return a different token each time", () => {
|
||||||
|
const result1 = generateSessionToken();
|
||||||
|
const result2 = generateSessionToken();
|
||||||
|
expect(result1).not.toEqual(result2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { objectKeys } from "../object";
|
import { objectEntries, objectKeys } from "../object";
|
||||||
|
|
||||||
|
const testObjects = [
|
||||||
|
{ a: 1, c: 3, b: 2 },
|
||||||
|
{ a: 1, b: 2 },
|
||||||
|
{ a: 1 },
|
||||||
|
{},
|
||||||
|
] as const;
|
||||||
|
|
||||||
describe("objectKeys should return all keys of an object", () => {
|
describe("objectKeys should return all keys of an object", () => {
|
||||||
it("should return all keys of an object", () => {
|
testObjects.forEach((obj) => {
|
||||||
const obj = { a: 1, b: 2, c: 3 };
|
it(`should return all keys of the object ${JSON.stringify(obj)}`, () => {
|
||||||
expect(objectKeys(obj)).toEqual(["a", "b", "c"]);
|
expect(objectKeys(obj)).toEqual(Object.keys(obj));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("objectEntries should return all entries of an object", () => {
|
||||||
|
testObjects.forEach((obj) => {
|
||||||
|
it(`should return all entries of the object ${JSON.stringify(obj)}`, () => {
|
||||||
|
expect(objectEntries(obj)).toEqual(Object.entries(obj));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export default {
|
|||||||
schema: "./schema",
|
schema: "./schema",
|
||||||
driver: "better-sqlite",
|
driver: "better-sqlite",
|
||||||
dbCredentials: { url: process.env.DB_URL! },
|
dbCredentials: { url: process.env.DB_URL! },
|
||||||
|
out: "./migrations",
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const schema = sqliteSchema;
|
|||||||
|
|
||||||
export * from "drizzle-orm";
|
export * from "drizzle-orm";
|
||||||
|
|
||||||
const sqlite = new Database(process.env.DB_URL);
|
export const sqlite = new Database(process.env.DB_URL);
|
||||||
|
|
||||||
export const db = drizzle(sqlite, { schema });
|
export const db = drizzle(sqlite, { schema });
|
||||||
|
|
||||||
|
|||||||
112
packages/db/migrations/0000_true_red_wolf.sql
Normal file
112
packages/db/migrations/0000_true_red_wolf.sql
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
CREATE TABLE `account` (
|
||||||
|
`userId` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`provider` text NOT NULL,
|
||||||
|
`providerAccountId` text NOT NULL,
|
||||||
|
`refresh_token` text,
|
||||||
|
`access_token` text,
|
||||||
|
`expires_at` integer,
|
||||||
|
`token_type` text,
|
||||||
|
`scope` text,
|
||||||
|
`id_token` text,
|
||||||
|
`session_state` text,
|
||||||
|
PRIMARY KEY(`provider`, `providerAccountId`),
|
||||||
|
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `board` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`is_public` integer DEFAULT false NOT NULL,
|
||||||
|
`page_title` text,
|
||||||
|
`meta_title` text,
|
||||||
|
`logo_image_url` text,
|
||||||
|
`favicon_image_url` text,
|
||||||
|
`background_image_url` text,
|
||||||
|
`background_image_attachment` text DEFAULT 'fixed' NOT NULL,
|
||||||
|
`background_image_repeat` text DEFAULT 'no-repeat' NOT NULL,
|
||||||
|
`background_image_size` text DEFAULT 'cover' NOT NULL,
|
||||||
|
`primary_color` text DEFAULT 'red' NOT NULL,
|
||||||
|
`secondary_color` text DEFAULT 'orange' NOT NULL,
|
||||||
|
`primary_shade` integer DEFAULT 6 NOT NULL,
|
||||||
|
`app_opacity` integer DEFAULT 100 NOT NULL,
|
||||||
|
`custom_css` text,
|
||||||
|
`show_right_sidebar` integer DEFAULT false NOT NULL,
|
||||||
|
`show_left_sidebar` integer DEFAULT false NOT NULL,
|
||||||
|
`column_count` integer DEFAULT 10 NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `integration_item` (
|
||||||
|
`item_id` text NOT NULL,
|
||||||
|
`integration_id` text NOT NULL,
|
||||||
|
PRIMARY KEY(`integration_id`, `item_id`),
|
||||||
|
FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `integrationSecret` (
|
||||||
|
`kind` text NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
`integration_id` text NOT NULL,
|
||||||
|
PRIMARY KEY(`integration_id`, `kind`),
|
||||||
|
FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `integration` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`url` text NOT NULL,
|
||||||
|
`kind` text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `item` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`section_id` text NOT NULL,
|
||||||
|
`kind` text NOT NULL,
|
||||||
|
`x_offset` integer NOT NULL,
|
||||||
|
`y_offset` integer NOT NULL,
|
||||||
|
`width` integer NOT NULL,
|
||||||
|
`height` integer NOT NULL,
|
||||||
|
`options` text DEFAULT '{"json": {}}' NOT NULL,
|
||||||
|
FOREIGN KEY (`section_id`) REFERENCES `section`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `section` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`board_id` text NOT NULL,
|
||||||
|
`kind` text NOT NULL,
|
||||||
|
`position` integer NOT NULL,
|
||||||
|
`name` text,
|
||||||
|
FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `session` (
|
||||||
|
`sessionToken` text PRIMARY KEY NOT NULL,
|
||||||
|
`userId` text NOT NULL,
|
||||||
|
`expires` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `user` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`name` text,
|
||||||
|
`email` text,
|
||||||
|
`emailVerified` integer,
|
||||||
|
`image` text,
|
||||||
|
`password` text,
|
||||||
|
`salt` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `verificationToken` (
|
||||||
|
`identifier` text NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`expires` integer NOT NULL,
|
||||||
|
PRIMARY KEY(`identifier`, `token`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `userId_idx` ON `account` (`userId`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `integration_secret__kind_idx` ON `integrationSecret` (`kind`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `integration_secret__updated_at_idx` ON `integrationSecret` (`updated_at`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `integration__kind_idx` ON `integration` (`kind`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `user_id_idx` ON `session` (`userId`);
|
||||||
696
packages/db/migrations/meta/0000_snapshot.json
Normal file
696
packages/db/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "c9ea435a-5bbf-4439-84a1-55d3e2581b13",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"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": {}
|
||||||
|
},
|
||||||
|
"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": "'red'"
|
||||||
|
},
|
||||||
|
"secondary_color": {
|
||||||
|
"name": "secondary_color",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'orange'"
|
||||||
|
},
|
||||||
|
"primary_shade": {
|
||||||
|
"name": "primary_shade",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 6
|
||||||
|
},
|
||||||
|
"app_opacity": {
|
||||||
|
"name": "app_opacity",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 100
|
||||||
|
},
|
||||||
|
"custom_css": {
|
||||||
|
"name": "custom_css",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"show_right_sidebar": {
|
||||||
|
"name": "show_right_sidebar",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"show_left_sidebar": {
|
||||||
|
"name": "show_left_sidebar",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"column_count": {
|
||||||
|
"name": "column_count",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/db/migrations/meta/_journal.json
Normal file
13
packages/db/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1707511343363,
|
||||||
|
"tag": "0000_true_red_wolf",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
"./client": "./client.ts",
|
"./client": "./client.ts",
|
||||||
"./schema/sqlite": "./schema/sqlite.ts"
|
"./schema/sqlite": "./schema/sqlite.ts",
|
||||||
|
"./test": "./test/index.ts"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "./index.ts",
|
"main": "./index.ts",
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
"clean": "rm -rf .turbo node_modules",
|
"clean": "rm -rf .turbo node_modules",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||||
|
"migration:generate": "drizzle-kit generate:sqlite",
|
||||||
"push": "drizzle-kit push:sqlite",
|
"push": "drizzle-kit push:sqlite",
|
||||||
"studio": "drizzle-kit studio",
|
"studio": "drizzle-kit studio",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
|
|||||||
14
packages/db/test/db-mock.ts
Normal file
14
packages/db/test/db-mock.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||||
|
|
||||||
|
import { schema } from "..";
|
||||||
|
|
||||||
|
export const createDb = () => {
|
||||||
|
const sqlite = new Database(":memory:");
|
||||||
|
const db = drizzle(sqlite, { schema });
|
||||||
|
migrate(db, {
|
||||||
|
migrationsFolder: "./packages/db/migrations",
|
||||||
|
});
|
||||||
|
return db;
|
||||||
|
};
|
||||||
1
packages/db/test/index.ts
Normal file
1
packages/db/test/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./db-mock";
|
||||||
14
packages/definitions/src/test/integration.spec.ts
Normal file
14
packages/definitions/src/test/integration.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { objectEntries } from "@homarr/common";
|
||||||
|
|
||||||
|
import { integrationDefs } from "../integration";
|
||||||
|
|
||||||
|
describe("Icon url's of integrations should be valid and return 200", () => {
|
||||||
|
objectEntries(integrationDefs).forEach(([integration, { iconUrl }]) => {
|
||||||
|
it.concurrent(`should return 200 for ${integration}`, async () => {
|
||||||
|
const res = await fetch(iconUrl);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,3 +2,4 @@ export const supportedLanguages = ["en", "de"] as const;
|
|||||||
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||||
|
|
||||||
export const defaultLocale = "en";
|
export const defaultLocale = "en";
|
||||||
|
export { languageMapping } from "./lang";
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ const saveGeneralSettingsSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.transform((value) => (value?.trim().length === 0 ? null : value)),
|
.transform((value) => (value?.trim().length === 0 ? null : value)),
|
||||||
|
boardId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveSchema = z.object({
|
const saveSchema = z.object({
|
||||||
name: boardNameSchema,
|
boardId: z.string(),
|
||||||
sections: z.array(createSectionSchema(commonItemSchema)),
|
sections: z.array(createSectionSchema(commonItemSchema)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
42
packages/widgets/src/test/translation.spec.ts
Normal file
42
packages/widgets/src/test/translation.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { objectEntries } from "@homarr/common";
|
||||||
|
import { languageMapping } from "@homarr/translation";
|
||||||
|
|
||||||
|
import { widgetImports } from "..";
|
||||||
|
|
||||||
|
describe("Widget properties with description should have matching translations", async () => {
|
||||||
|
const enTranslation = await languageMapping().en();
|
||||||
|
objectEntries(widgetImports).forEach(([key, value]) => {
|
||||||
|
Object.entries(value.definition.options).forEach(
|
||||||
|
([optionKey, optionValue]: [string, { withDescription?: boolean }]) => {
|
||||||
|
it(`should have matching translations for ${optionKey} option description of ${key} widget`, () => {
|
||||||
|
const option = enTranslation.default.widget[key].option;
|
||||||
|
if (!(optionKey in option)) {
|
||||||
|
throw new Error(`Option ${optionKey} not found in translation`);
|
||||||
|
}
|
||||||
|
const value = option[optionKey as keyof typeof option];
|
||||||
|
|
||||||
|
expect("description" in value).toBe(optionValue.withDescription);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Widget properties should have matching name translations", async () => {
|
||||||
|
const enTranslation = await languageMapping().en();
|
||||||
|
objectEntries(widgetImports).forEach(([key, value]) => {
|
||||||
|
Object.keys(value.definition.options).forEach((optionKey) => {
|
||||||
|
it(`should have matching translations for ${optionKey} option name of ${key} widget`, () => {
|
||||||
|
const option = enTranslation.default.widget[key].option;
|
||||||
|
if (!(optionKey in option)) {
|
||||||
|
throw new Error(`Option ${optionKey} not found in translation`);
|
||||||
|
}
|
||||||
|
const value = option[optionKey as keyof typeof option];
|
||||||
|
|
||||||
|
expect("label" in value).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
297
pnpm-lock.yaml
generated
297
pnpm-lock.yaml
generated
@@ -15,6 +15,9 @@ importers:
|
|||||||
'@homarr/prettier-config':
|
'@homarr/prettier-config':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:tooling/prettier
|
version: link:tooling/prettier
|
||||||
|
'@testing-library/react-hooks':
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.0.1(react@17.0.2)
|
||||||
'@turbo/gen':
|
'@turbo/gen':
|
||||||
specifier: ^1.12.3
|
specifier: ^1.12.3
|
||||||
version: 1.12.3(@types/node@20.11.17)(typescript@5.3.3)
|
version: 1.12.3(@types/node@20.11.17)(typescript@5.3.3)
|
||||||
@@ -30,6 +33,9 @@ importers:
|
|||||||
cross-env:
|
cross-env:
|
||||||
specifier: ^7.0.3
|
specifier: ^7.0.3
|
||||||
version: 7.0.3
|
version: 7.0.3
|
||||||
|
jsdom:
|
||||||
|
specifier: ^24.0.0
|
||||||
|
version: 24.0.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.2.5
|
||||||
version: 3.2.5
|
version: 3.2.5
|
||||||
@@ -44,7 +50,7 @@ importers:
|
|||||||
version: 4.3.1(typescript@5.3.3)(vite@5.0.12)
|
version: 4.3.1(typescript@5.3.3)(vite@5.0.12)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)
|
version: 1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)(jsdom@24.0.0)
|
||||||
|
|
||||||
apps/nextjs:
|
apps/nextjs:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -326,7 +332,7 @@ importers:
|
|||||||
version: 9.4.0
|
version: 9.4.0
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.29.3
|
specifier: ^0.29.3
|
||||||
version: 0.29.3(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.0)
|
version: 0.29.3(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.0)(react@17.0.2)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@homarr/eslint-config':
|
'@homarr/eslint-config':
|
||||||
specifier: workspace:^0.2.0
|
specifier: workspace:^0.2.0
|
||||||
@@ -961,7 +967,6 @@ packages:
|
|||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.0
|
regenerator-runtime: 0.14.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@babel/template@7.22.15:
|
/@babel/template@7.22.15:
|
||||||
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
|
||||||
@@ -2150,6 +2155,27 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@testing-library/react-hooks@8.0.1(react@17.0.2):
|
||||||
|
resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^16.9.0 || ^17.0.0
|
||||||
|
react: ^16.9.0 || ^17.0.0
|
||||||
|
react-dom: ^16.9.0 || ^17.0.0
|
||||||
|
react-test-renderer: ^16.9.0 || ^17.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-test-renderer:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.2
|
||||||
|
react: 17.0.2
|
||||||
|
react-error-boundary: 3.1.4(react@17.0.2)
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@tiptap/core@2.2.2(@tiptap/pm@2.2.2):
|
/@tiptap/core@2.2.2(@tiptap/pm@2.2.2):
|
||||||
resolution: {integrity: sha512-fec26LtNgYFGhKzEA9+Of+qLKIKUxDL/XZQofoPcxP71NffcmpZ+ZjAx9NjnvuYtvylUSySZiPauY6WhN3aprw==}
|
resolution: {integrity: sha512-fec26LtNgYFGhKzEA9+Of+qLKIKUxDL/XZQofoPcxP71NffcmpZ+ZjAx9NjnvuYtvylUSySZiPauY6WhN3aprw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2914,7 +2940,7 @@ packages:
|
|||||||
std-env: 3.7.0
|
std-env: 3.7.0
|
||||||
test-exclude: 6.0.0
|
test-exclude: 6.0.0
|
||||||
v8-to-istanbul: 9.2.0
|
v8-to-istanbul: 9.2.0
|
||||||
vitest: 1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)
|
vitest: 1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)(jsdom@24.0.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2961,7 +2987,7 @@ packages:
|
|||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
sirv: 2.0.4
|
sirv: 2.0.4
|
||||||
vitest: 1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)
|
vitest: 1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)(jsdom@24.0.0)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@vitest/utils@1.2.2:
|
/@vitest/utils@1.2.2:
|
||||||
@@ -3232,6 +3258,10 @@ packages:
|
|||||||
has-symbols: 1.0.3
|
has-symbols: 1.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/available-typed-arrays@1.0.5:
|
/available-typed-arrays@1.0.5:
|
||||||
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
|
resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3589,6 +3619,13 @@ packages:
|
|||||||
text-hex: 1.0.0
|
text-hex: 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/commander@10.0.1:
|
/commander@10.0.1:
|
||||||
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -3670,6 +3707,13 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cssstyle@4.0.1:
|
||||||
|
resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
rrweb-cssom: 0.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/csstype@3.1.2:
|
/csstype@3.1.2:
|
||||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||||
|
|
||||||
@@ -3693,6 +3737,14 @@ packages:
|
|||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/data-urls@5.0.0:
|
||||||
|
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
whatwg-mimetype: 4.0.0
|
||||||
|
whatwg-url: 14.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/dayjs@1.11.10:
|
/dayjs@1.11.10:
|
||||||
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3719,6 +3771,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
|
||||||
|
/decimal.js@10.4.3:
|
||||||
|
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/decompress-response@6.0.0:
|
/decompress-response@6.0.0:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3800,6 +3856,11 @@ packages:
|
|||||||
slash: 3.0.0
|
slash: 3.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/delegates@1.0.0:
|
/delegates@1.0.0:
|
||||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -3924,7 +3985,7 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/drizzle-orm@0.29.3(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.0):
|
/drizzle-orm@0.29.3(@types/better-sqlite3@7.6.9)(better-sqlite3@9.4.0)(react@17.0.2):
|
||||||
resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==}
|
resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@aws-sdk/client-rds-data': '>=3'
|
'@aws-sdk/client-rds-data': '>=3'
|
||||||
@@ -3997,6 +4058,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/better-sqlite3': 7.6.9
|
'@types/better-sqlite3': 7.6.9
|
||||||
better-sqlite3: 9.4.0
|
better-sqlite3: 9.4.0
|
||||||
|
react: 17.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
@@ -4030,7 +4092,6 @@ packages:
|
|||||||
/entities@4.5.0:
|
/entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/env-paths@3.0.0:
|
/env-paths@3.0.0:
|
||||||
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
||||||
@@ -4681,6 +4742,15 @@ packages:
|
|||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/form-data@4.0.0:
|
||||||
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fs-constants@1.0.0:
|
/fs-constants@1.0.0:
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -5009,6 +5079,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==}
|
resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/html-encoding-sniffer@4.0.0:
|
||||||
|
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
whatwg-encoding: 3.1.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/html-escaper@2.0.2:
|
/html-escaper@2.0.2:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5060,6 +5137,13 @@ packages:
|
|||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/iconv-lite@0.6.3:
|
||||||
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ieee754@1.2.1:
|
/ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
@@ -5304,6 +5388,10 @@ packages:
|
|||||||
isobject: 3.0.1
|
isobject: 3.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/is-potential-custom-element-name@1.0.1:
|
||||||
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-promise@2.2.2:
|
/is-promise@2.2.2:
|
||||||
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
|
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -5499,6 +5587,42 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
/jsdom@24.0.0:
|
||||||
|
resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
canvas: ^2.11.2
|
||||||
|
peerDependenciesMeta:
|
||||||
|
canvas:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
cssstyle: 4.0.1
|
||||||
|
data-urls: 5.0.0
|
||||||
|
decimal.js: 10.4.3
|
||||||
|
form-data: 4.0.0
|
||||||
|
html-encoding-sniffer: 4.0.0
|
||||||
|
http-proxy-agent: 7.0.0
|
||||||
|
https-proxy-agent: 7.0.2
|
||||||
|
is-potential-custom-element-name: 1.0.1
|
||||||
|
nwsapi: 2.2.7
|
||||||
|
parse5: 7.1.2
|
||||||
|
rrweb-cssom: 0.6.0
|
||||||
|
saxes: 6.0.0
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
tough-cookie: 4.1.3
|
||||||
|
w3c-xmlserializer: 5.0.0
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
whatwg-encoding: 3.1.1
|
||||||
|
whatwg-mimetype: 4.0.0
|
||||||
|
whatwg-url: 14.0.0
|
||||||
|
ws: 8.16.0
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
dev: true
|
||||||
|
|
||||||
/jsesc@2.5.2:
|
/jsesc@2.5.2:
|
||||||
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -5669,7 +5793,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
dev: false
|
|
||||||
|
|
||||||
/loupe@2.3.7:
|
/loupe@2.3.7:
|
||||||
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
||||||
@@ -5804,6 +5927,18 @@ packages:
|
|||||||
braces: 3.0.2
|
braces: 3.0.2
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
/mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/mimic-fn@2.1.0:
|
/mimic-fn@2.1.0:
|
||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -6091,6 +6226,10 @@ packages:
|
|||||||
set-blocking: 2.0.0
|
set-blocking: 2.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/nwsapi@2.2.7:
|
||||||
|
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/oauth4webapi@2.10.3:
|
/oauth4webapi@2.10.3:
|
||||||
resolution: {integrity: sha512-9FkXEXfzVKzH63GUOZz1zMr3wBaICSzk6DLXx+CGdrQ10ItNk2ePWzYYc1fdmKq1ayGFb2aX97sRCoZ2s0mkDw==}
|
resolution: {integrity: sha512-9FkXEXfzVKzH63GUOZz1zMr3wBaICSzk6DLXx+CGdrQ10ItNk2ePWzYYc1fdmKq1ayGFb2aX97sRCoZ2s0mkDw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6098,7 +6237,6 @@ packages:
|
|||||||
/object-assign@4.1.1:
|
/object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: false
|
|
||||||
|
|
||||||
/object-inspect@1.12.3:
|
/object-inspect@1.12.3:
|
||||||
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
|
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
|
||||||
@@ -6340,6 +6478,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
/parse5@7.1.2:
|
||||||
|
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||||
|
dependencies:
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/pascal-case@2.0.1:
|
/pascal-case@2.0.1:
|
||||||
resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==}
|
resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6720,6 +6864,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/psl@1.9.0:
|
||||||
|
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/pump@3.0.0:
|
/pump@3.0.0:
|
||||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6736,6 +6884,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
/punycode@2.3.1:
|
||||||
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/querystringify@2.2.0:
|
||||||
|
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/queue-microtask@1.2.3:
|
/queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -6758,6 +6915,16 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-error-boundary@3.1.4(react@17.0.2):
|
||||||
|
resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
|
||||||
|
engines: {node: '>=10', npm: '>=6'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.13.1'
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.2
|
||||||
|
react: 17.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6870,6 +7037,13 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react@17.0.2:
|
||||||
|
resolution: {integrity: sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
object-assign: 4.1.1
|
||||||
|
|
||||||
/react@18.2.0:
|
/react@18.2.0:
|
||||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -6942,6 +7116,10 @@ packages:
|
|||||||
rc: 1.2.8
|
rc: 1.2.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/requires-port@1.0.0:
|
||||||
|
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -7010,6 +7188,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
|
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/rrweb-cssom@0.6.0:
|
||||||
|
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/run-async@2.4.1:
|
/run-async@2.4.1:
|
||||||
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
@@ -7083,6 +7265,13 @@ packages:
|
|||||||
source-map-js: 1.0.2
|
source-map-js: 1.0.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/saxes@6.0.0:
|
||||||
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
|
engines: {node: '>=v12.22.7'}
|
||||||
|
dependencies:
|
||||||
|
xmlchars: 2.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/scheduler@0.23.0:
|
/scheduler@0.23.0:
|
||||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7408,6 +7597,10 @@ packages:
|
|||||||
upper-case: 1.1.3
|
upper-case: 1.1.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/symbol-tree@3.2.4:
|
||||||
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tabbable@6.2.0:
|
/tabbable@6.2.0:
|
||||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -7536,10 +7729,27 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tough-cookie@4.1.3:
|
||||||
|
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
psl: 1.9.0
|
||||||
|
punycode: 2.3.0
|
||||||
|
universalify: 0.2.0
|
||||||
|
url-parse: 1.5.10
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tr46@0.0.3:
|
/tr46@0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tr46@5.0.0:
|
||||||
|
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/triple-beam@1.4.1:
|
/triple-beam@1.4.1:
|
||||||
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
|
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
|
||||||
engines: {node: '>= 14.0.0'}
|
engines: {node: '>= 14.0.0'}
|
||||||
@@ -7798,6 +8008,11 @@ packages:
|
|||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/universalify@0.2.0:
|
||||||
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
|
engines: {node: '>= 4.0.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/universalify@2.0.0:
|
/universalify@2.0.0:
|
||||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -7846,6 +8061,13 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
|
|
||||||
|
/url-parse@1.5.10:
|
||||||
|
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||||
|
dependencies:
|
||||||
|
querystringify: 2.2.0
|
||||||
|
requires-port: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/use-callback-ref@1.3.0(@types/react@18.2.55)(react@18.2.0):
|
/use-callback-ref@1.3.0(@types/react@18.2.55)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
|
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -8009,7 +8231,7 @@ packages:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitest@1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2):
|
/vitest@1.2.2(@types/node@20.11.17)(@vitest/ui@1.2.2)(jsdom@24.0.0):
|
||||||
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -8046,6 +8268,7 @@ packages:
|
|||||||
chai: 4.4.1
|
chai: 4.4.1
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
execa: 8.0.1
|
execa: 8.0.1
|
||||||
|
jsdom: 24.0.0
|
||||||
local-pkg: 0.5.0
|
local-pkg: 0.5.0
|
||||||
magic-string: 0.30.6
|
magic-string: 0.30.6
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
@@ -8071,6 +8294,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/w3c-xmlserializer@5.0.0:
|
||||||
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/wcwidth@1.0.1:
|
/wcwidth@1.0.1:
|
||||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8081,6 +8311,31 @@ packages:
|
|||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/webidl-conversions@7.0.0:
|
||||||
|
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-encoding@3.1.1:
|
||||||
|
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-mimetype@4.0.0:
|
||||||
|
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-url@14.0.0:
|
||||||
|
resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
tr46: 5.0.0
|
||||||
|
webidl-conversions: 7.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/whatwg-url@5.0.0:
|
/whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8217,6 +8472,28 @@ packages:
|
|||||||
/wrappy@1.0.2:
|
/wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
/ws@8.16.0:
|
||||||
|
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/xml-name-validator@5.0.0:
|
||||||
|
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/xmlchars@2.2.0:
|
||||||
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/yallist@3.1.1:
|
/yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,13 @@
|
|||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"noUncheckedIndexedAccess": true
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"*": [
|
||||||
|
"node_modules/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "build", "dist", ".next"]
|
"exclude": ["node_modules", "build", "dist", ".next"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { configDefaults, defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tsconfigPaths()],
|
plugins: [react(), tsconfigPaths()],
|
||||||
test: {
|
test: {
|
||||||
|
setupFiles: ["./vitest.setup.ts"],
|
||||||
|
environment: "jsdom",
|
||||||
include: ["**/*.spec.ts"],
|
include: ["**/*.spec.ts"],
|
||||||
poolOptions: {
|
poolOptions: {
|
||||||
threads: {
|
threads: {
|
||||||
|
|||||||
3
vitest.setup.ts
Normal file
3
vitest.setup.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("server-only", () => ({ default: undefined }));
|
||||||
Reference in New Issue
Block a user