refactor: remove central validation export to improve typescript performance (#2810)

* refactor: remove central validation export to improve typescript performance

* fix: missing package exports change in validation package

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2025-04-06 12:37:28 +02:00
committed by GitHub
parent c1cd563048
commit 75ba3f2ae7
81 changed files with 450 additions and 582 deletions

View File

@@ -9,7 +9,7 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation";
import { userRegistrationSchema } from "@homarr/validation/user";
interface RegistrationFormProps {
invite: {
@@ -22,7 +22,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
const t = useScopedI18n("user");
const router = useRouter();
const { mutate, isPending } = clientApi.user.register.useMutation();
const form = useZodForm(validation.user.registration, {
const form = useZodForm(userRegistrationSchema, {
initialValues: {
username: "",
password: "",
@@ -30,7 +30,7 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
},
});
const handleSubmit = (values: z.infer<typeof validation.user.registration>) => {
const handleSubmit = (values: z.infer<typeof userRegistrationSchema>) => {
mutate(
{
...values,

View File

@@ -13,7 +13,7 @@ import type { useForm } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { userSignInSchema } from "@homarr/validation/user";
interface LoginFormProps {
providers: string[];
@@ -22,7 +22,7 @@ interface LoginFormProps {
callbackUrl: string;
}
const extendedValidation = validation.user.signIn.extend({ provider: z.enum(["credentials", "ldap"]) });
const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credentials", "ldap"]) });
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
const t = useScopedI18n("user");

View File

@@ -20,7 +20,7 @@ import { useDisclosure } from "@mantine/hooks";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types";
import { generateColors } from "../../(content)/_theme";
@@ -35,7 +35,7 @@ const hexRegex = /^#[0-9a-fA-F]{6}$/;
const progressPercentageLabel = (value: number) => `${value}%`;
export const ColorSettingsContent = ({ board }: Props) => {
const form = useZodForm(validation.board.savePartialSettings, {
const form = useZodForm(boardSavePartialSettingsSchema, {
initialValues: {
primaryColor: board.primaryColor,
secondaryColor: board.secondaryColor,

View File

@@ -12,7 +12,7 @@ import type { TranslationObject } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { SelectItemWithDescriptionBadge } from "@homarr/ui";
import { SelectWithDescriptionBadge } from "@homarr/ui";
import { validation } from "@homarr/validation";
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types";
import { useSavePartialSettingsMutation } from "./_shared";
@@ -24,7 +24,7 @@ export const BackgroundSettingsContent = ({ board }: Props) => {
const t = useI18n();
const { data: session } = useSession();
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(validation.board.savePartialSettings, {
const form = useZodForm(boardSavePartialSettingsSchema, {
initialValues: {
backgroundImageUrl: board.backgroundImageUrl ?? "",
backgroundImageAttachment: board.backgroundImageAttachment,

View File

@@ -8,7 +8,7 @@ import { useUpdateBoard } from "@homarr/boards/updater";
import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types";
@@ -28,7 +28,7 @@ export const GeneralSettingsContent = ({ board }: Props) => {
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
const form = useZodForm(
validation.board.savePartialSettings
boardSavePartialSettingsSchema
.pick({
pageTitle: true,
logoImageUrl: true,

View File

@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { createId } from "@homarr/db/client";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { boardSaveLayoutsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types";
@@ -22,7 +22,7 @@ export const LayoutSettingsContent = ({ board }: Props) => {
void utils.board.getHomeBoard.invalidate();
},
});
const form = useZodForm(validation.board.saveLayouts.omit({ id: true }).required(), {
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
initialValues: {
layouts: board.layouts,
},

View File

@@ -8,18 +8,18 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { groupCreateSchema } from "@homarr/validation/group";
export const InitGroup = () => {
const t = useI18n();
const { mutateAsync } = clientApi.group.createInitialExternalGroup.useMutation();
const form = useZodForm(validation.group.create, {
const form = useZodForm(groupCreateSchema, {
initialValues: {
name: "",
},
});
const handleSubmitAsync = async (values: z.infer<typeof validation.group.create>) => {
const handleSubmitAsync = async (values: z.infer<typeof groupCreateSchema>) => {
await mutateAsync(values, {
async onSuccess() {
await revalidatePathActionAsync("/init");

View File

@@ -12,13 +12,13 @@ import type { CheckboxProps } from "@homarr/form/types";
import { defaultServerSettings } from "@homarr/server-settings";
import type { TranslationObject } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { settingsInitSchema } from "@homarr/validation/settings";
export const InitSettings = () => {
const tSection = useScopedI18n("management.page.settings.section");
const t = useI18n();
const { mutateAsync } = clientApi.serverSettings.initSettings.useMutation();
const form = useZodForm(validation.settings.init, { initialValues: defaultServerSettings });
const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings });
form.watch("analytics.enableGeneral", ({ value }) => {
if (!value) {
@@ -30,7 +30,7 @@ export const InitSettings = () => {
}
});
const handleSubmitAsync = async (values: z.infer<typeof validation.settings.init>) => {
const handleSubmitAsync = async (values: z.infer<typeof settingsInitSchema>) => {
await mutateAsync(values, {
async onSuccess() {
await revalidatePathActionAsync("/init");

View File

@@ -10,13 +10,13 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation";
import { userInitSchema } from "@homarr/validation/user";
export const InitUserForm = () => {
const t = useScopedI18n("user");
const tUser = useScopedI18n("init.step.user");
const { mutateAsync, isPending } = clientApi.user.initUser.useMutation();
const form = useZodForm(validation.user.init, {
const form = useZodForm(userInitSchema, {
initialValues: {
username: "",
password: "",
@@ -74,4 +74,4 @@ export const InitUserForm = () => {
);
};
type FormType = z.infer<typeof validation.user.init>;
type FormType = z.infer<typeof userInitSchema>;

View File

@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { AppForm } from "@homarr/forms-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation";
import type { appManageSchema } from "@homarr/validation/app";
interface AppEditFormProps {
app: RouterOutputs["app"]["byId"];
@@ -40,7 +40,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
});
const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => {
(values: z.infer<typeof appManageSchema>) => {
mutate({
id: app.id,
...values,

View File

@@ -14,7 +14,7 @@ import { convertIntegrationTestConnectionError } from "@homarr/integrations/clie
import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { integrationUpdateSchema } from "@homarr/validation/integration";
import { SecretCard } from "../../_components/secrets/integration-secret-card";
import { IntegrationSecretInput } from "../../_components/secrets/integration-secret-inputs";
@@ -32,7 +32,7 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
) ?? getDefaultSecretKinds(integration.kind);
const router = useRouter();
const form = useZodForm(validation.integration.update.omit({ id: true }), {
const form = useZodForm(integrationUpdateSchema.omit({ id: true }), {
initialValues: {
name: integration.name,
url: integration.url,
@@ -141,4 +141,4 @@ export const EditIntegrationForm = ({ integration }: EditIntegrationForm) => {
);
};
type FormType = Omit<z.infer<typeof validation.integration.update>, "id">;
type FormType = Omit<z.infer<typeof integrationUpdateSchema>, "id">;

View File

@@ -27,20 +27,21 @@ import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { appHrefSchema } from "@homarr/validation/app";
import { integrationCreateSchema } from "@homarr/validation/integration";
import { IntegrationSecretInput } from "../_components/secrets/integration-secret-inputs";
interface NewIntegrationFormProps {
searchParams: Partial<z.infer<typeof validation.integration.create>> & {
searchParams: Partial<z.infer<typeof integrationCreateSchema>> & {
kind: IntegrationKind;
};
}
const formSchema = validation.integration.create.omit({ kind: true }).and(
const formSchema = integrationCreateSchema.omit({ kind: true }).and(
z.object({
createApp: z.boolean(),
appHref: validation.app.manage.shape.href,
appHref: appHrefSchema,
}),
);

View File

@@ -7,14 +7,14 @@ import type { IntegrationKind } from "@homarr/definitions";
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { IntegrationAvatar } from "@homarr/ui";
import type { validation } from "@homarr/validation";
import type { integrationCreateSchema } from "@homarr/validation/integration";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NewIntegrationForm } from "./_integration-new-form";
interface NewIntegrationPageProps {
searchParams: Promise<
Partial<z.infer<typeof validation.integration.create>> & {
Partial<z.infer<typeof integrationCreateSchema>> & {
kind: IntegrationKind;
}
>;

View File

@@ -12,9 +12,9 @@ import { useZodForm } from "@homarr/form";
import { IconPicker } from "@homarr/forms-collection";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { searchEngineManageSchema } from "@homarr/validation/search-engine";
type FormType = z.infer<typeof validation.searchEngine.manage>;
type FormType = z.infer<typeof searchEngineManageSchema>;
interface SearchEngineFormProps {
submitButtonTranslation: (t: TranslationFunction) => string;
@@ -30,7 +30,7 @@ export const SearchEngineForm = (props: SearchEngineFormProps) => {
const [integrationData] = clientApi.integration.allThatSupportSearch.useSuspenseQuery();
const form = useZodForm(validation.searchEngine.manage, {
const form = useZodForm(searchEngineManageSchema, {
initialValues: initialValues ?? {
name: "",
short: "",

View File

@@ -10,7 +10,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation";
import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
import { SearchEngineForm } from "../../_form";
@@ -41,7 +41,7 @@ export const SearchEngineEditForm = ({ searchEngine }: SearchEngineEditFormProps
});
const handleSubmit = useCallback(
(values: z.infer<typeof validation.searchEngine.manage>) => {
(values: z.infer<typeof searchEngineManageSchema>) => {
mutate({
id: searchEngine.id,
...values,

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation";
import type { searchEngineManageSchema } from "@homarr/validation/search-engine";
import { SearchEngineForm } from "../_form";
@@ -35,7 +35,7 @@ export const SearchEngineNewForm = () => {
});
const handleSubmit = useCallback(
(values: z.infer<typeof validation.searchEngine.manage>) => {
(values: z.infer<typeof searchEngineManageSchema>) => {
mutate(values);
},
[mutate],

View File

@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { superRefineCertificateFile } from "@homarr/validation";
import { superRefineCertificateFile } from "@homarr/validation/certificates";
export const AddCertificateButton = () => {
const { openModal } = useModalAction(AddCertificateModal);

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
@@ -40,7 +40,7 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
});
},
});
const form = useZodForm(validation.user.changeHomeBoards, {
const form = useZodForm(userChangeHomeBoardsSchema, {
initialValues: {
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
@@ -82,4 +82,4 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro
);
};
type FormType = z.infer<typeof validation.user.changeHomeBoards>;
type FormType = z.infer<typeof userChangeHomeBoardsSchema>;

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"];
@@ -37,7 +37,7 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
});
},
});
const form = useZodForm(validation.user.changeSearchPreferences, {
const form = useZodForm(userChangeSearchPreferencesSchema, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
@@ -75,4 +75,4 @@ export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeS
);
};
type FormType = z.infer<typeof validation.user.changeSearchPreferences>;
type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;

View File

@@ -12,7 +12,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
dayjs.extend(localeData);
@@ -42,7 +42,7 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
});
},
});
const form = useZodForm(validation.user.firstDayOfWeek, {
const form = useZodForm(userFirstDayOfWeekSchema, {
initialValues: {
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
},
@@ -80,4 +80,4 @@ export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
);
};
type FormType = z.infer<typeof validation.user.firstDayOfWeek>;
type FormType = z.infer<typeof userFirstDayOfWeekSchema>;

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { userPingIconsEnabledSchema } from "@homarr/validation/user";
interface PingIconsEnabledProps {
user: RouterOutputs["user"]["getById"];
@@ -35,7 +35,7 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
});
},
});
const form = useZodForm(validation.user.pingIconsEnabled, {
const form = useZodForm(userPingIconsEnabledSchema, {
initialValues: {
pingIconsEnabled: user.pingIconsEnabled,
},
@@ -66,4 +66,4 @@ export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
);
};
type FormType = z.infer<typeof validation.user.pingIconsEnabled>;
type FormType = z.infer<typeof userPingIconsEnabledSchema>;

View File

@@ -9,7 +9,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { userEditProfileSchema } from "@homarr/validation/user";
interface UserProfileFormProps {
user: RouterOutputs["user"]["getById"];
@@ -43,7 +43,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
});
},
});
const form = useZodForm(validation.user.editProfile.omit({ id: true }), {
const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
initialValues: {
name: user.name ?? "",
email: user.email ?? "",

View File

@@ -10,7 +10,7 @@ import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import { validation } from "@homarr/validation";
import { userChangePasswordSchema } from "@homarr/validation/user";
interface ChangePasswordFormProps {
user: RouterOutputs["user"]["getById"];
@@ -34,7 +34,7 @@ export const ChangePasswordForm = ({ user }: ChangePasswordFormProps) => {
});
},
});
const form = useZodForm(validation.user.changePassword, {
const form = useZodForm(userChangePasswordSchema, {
initialValues: {
/* Require previous password if the current user want's to change his password */
previousPassword: session?.user.id === user.id ? "" : "_",

View File

@@ -27,8 +27,8 @@ import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { validation } from "@homarr/validation";
import { createCustomErrorParams } from "@homarr/validation/form";
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { userPasswordSchema } from "@homarr/validation/user";
import { GroupSelectModal } from "~/components/access/group-select-modal";
import { StepperNavigationComponent } from "./stepper-navigation";
@@ -84,7 +84,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
const securityForm = useZodForm(
z
.object({
password: validation.user.password,
password: userPasswordSchema,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {

View File

@@ -8,7 +8,7 @@ import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { groupUpdateSchema } from "@homarr/validation/group";
interface RenameGroupFormProps {
group: {
@@ -21,7 +21,7 @@ interface RenameGroupFormProps {
export const RenameGroupForm = ({ group, disabled }: RenameGroupFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.group.updateGroup.useMutation();
const form = useZodForm(validation.group.update.pick({ name: true }), {
const form = useZodForm(groupUpdateSchema.pick({ name: true }), {
initialValues: {
name: group.name,
},

View File

@@ -6,7 +6,7 @@ import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { groupSettingsSchema } from "@homarr/validation/group";
import { BoardSelect } from "~/components/board/board-select";
@@ -19,7 +19,7 @@ interface GroupHomeBoardsProps {
export const GroupHomeBoards = ({ homeBoardId, mobileHomeBoardId, groupId }: GroupHomeBoardsProps) => {
const t = useI18n();
const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId });
const form = useZodForm(validation.group.settings.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
const form = useZodForm(groupSettingsSchema.pick({ homeBoardId: true, mobileHomeBoardId: true }), {
initialValues: {
homeBoardId,
mobileHomeBoardId,

View File

@@ -11,7 +11,7 @@ import { useModalAction } from "@homarr/modals";
import { showSuccessNotification } from "@homarr/notifications";
import { useSettings } from "@homarr/settings";
import { useScopedI18n } from "@homarr/translation/client";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetError } from "@homarr/widgets/errors";
import { WidgetEditModal } from "@homarr/widgets/modals";

View File

@@ -1,7 +1,7 @@
import { useCallback } from "react";
import { useUpdateBoard } from "@homarr/boards/updater";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import type { CreateItemInput } from "./actions/create-item";
import { createItemCallback } from "./actions/create-item";

View File

@@ -7,7 +7,7 @@ import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { boardRenameSchema } from "@homarr/validation/board";
interface InnerProps {
id: string;
@@ -26,7 +26,7 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
void utils.board.getHomeBoard.invalidate();
},
});
const form = useZodForm(validation.board.rename.omit({ id: true }), {
const form = useZodForm(boardRenameSchema.omit({ id: true }), {
initialValues: {
name: innerProps.previousName,
},
@@ -66,4 +66,4 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
});
type FormType = Omit<z.infer<(typeof validation)["board"]["rename"]>, "id">;
type FormType = Omit<z.infer<typeof boardRenameSchema>, "id">;

View File

@@ -2,7 +2,7 @@ import { useCallback } from "react";
import type { z } from "zod";
import { useUpdateBoard } from "@homarr/boards/updater";
import type { dynamicSectionOptionsSchema } from "@homarr/validation";
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
import { addDynamicSectionCallback } from "./actions/add-dynamic-section";
import type { RemoveDynamicSectionInput } from "./actions/remove-dynamic-section";

View File

@@ -6,7 +6,7 @@ import type { z } from "zod";
import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { dynamicSectionOptionsSchema } from "@homarr/validation";
import { dynamicSectionOptionsSchema } from "@homarr/validation/shared";
interface ModalProps {
value: z.infer<typeof dynamicSectionOptionsSchema>;

View File

@@ -5,7 +5,8 @@ import { asc, createId, eq, inArray, like } from "@homarr/db";
import { apps } from "@homarr/db/schema";
import { selectAppSchema } from "@homarr/db/validationSchemas";
import { getIconForName } from "@homarr/icons";
import { validation } from "@homarr/validation";
import { appCreateManySchema, appEditSchema, appManageSchema } from "@homarr/validation/app";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import { convertIntersectionToZodObject } from "../schema-merger";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
@@ -15,7 +16,7 @@ const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@mas
export const appRouter = createTRPCRouter({
getPaginated: protectedProcedure
.input(validation.common.paginated)
.input(paginatedSchema)
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
.meta({ openapi: { method: "GET", path: "/api/apps/paginated", tags: ["apps"], protect: true } })
.query(async ({ input, ctx }) => {
@@ -83,7 +84,7 @@ export const appRouter = createTRPCRouter({
});
}),
byId: publicProcedure
.input(validation.common.byId)
.input(byIdSchema)
.output(selectAppSchema)
.meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.query(async ({ ctx, input }) => {
@@ -115,7 +116,7 @@ export const appRouter = createTRPCRouter({
}),
create: permissionRequiredProcedure
.requiresPermission("app-create")
.input(validation.app.manage)
.input(appManageSchema)
.output(z.object({ appId: z.string() }))
.meta({ openapi: { method: "POST", path: "/api/apps", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => {
@@ -133,7 +134,7 @@ export const appRouter = createTRPCRouter({
}),
createMany: permissionRequiredProcedure
.requiresPermission("app-create")
.input(validation.app.createMany)
.input(appCreateManySchema)
.output(z.void())
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(apps).values(
@@ -148,7 +149,7 @@ export const appRouter = createTRPCRouter({
}),
update: permissionRequiredProcedure
.requiresPermission("app-modify-all")
.input(convertIntersectionToZodObject(validation.app.edit))
.input(convertIntersectionToZodObject(appEditSchema))
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.mutation(async ({ ctx, input }) => {
@@ -178,7 +179,7 @@ export const appRouter = createTRPCRouter({
.requiresPermission("app-full-all")
.output(z.void())
.meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } })
.input(validation.common.byId)
.input(byIdSchema)
.mutation(async ({ ctx, input }) => {
await ctx.db.delete(apps).where(eq(apps.id, input.id));
}),

View File

@@ -37,8 +37,21 @@ import {
import { importOldmarrAsync } from "@homarr/old-import";
import { importJsonFileSchema } from "@homarr/old-import/shared";
import { oldmarrConfigSchema } from "@homarr/old-schema";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation";
import {
boardByNameSchema,
boardChangeVisibilitySchema,
boardCreateSchema,
boardDuplicateSchema,
boardRenameSchema,
boardSaveLayoutsSchema,
boardSavePartialSettingsSchema,
boardSavePermissionsSchema,
boardSaveSchema,
} from "@homarr/validation/board";
import { byIdSchema } from "@homarr/validation/common";
import { zodUnionFromArray } from "@homarr/validation/enums";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access";
@@ -247,7 +260,7 @@ export const boardRouter = createTRPCRouter({
}),
createBoard: permissionRequiredProcedure
.requiresPermission("board-create")
.input(validation.board.create)
.input(boardCreateSchema)
.mutation(async ({ ctx, input }) => {
const boardId = createId();
@@ -291,7 +304,7 @@ export const boardRouter = createTRPCRouter({
}),
duplicateBoard: permissionRequiredProcedure
.requiresPermission("board-create")
.input(validation.board.duplicate)
.input(boardDuplicateSchema)
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "view");
await noBoardWithSimilarNameAsync(ctx.db, input.name);
@@ -506,34 +519,32 @@ export const boardRouter = createTRPCRouter({
},
});
}),
renameBoard: protectedProcedure.input(validation.board.rename).mutation(async ({ ctx, input }) => {
renameBoard: protectedProcedure.input(boardRenameSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
await noBoardWithSimilarNameAsync(ctx.db, input.name, [input.id]);
await ctx.db.update(boards).set({ name: input.name }).where(eq(boards.id, input.id));
}),
changeBoardVisibility: protectedProcedure
.input(validation.board.changeVisibility)
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
changeBoardVisibility: protectedProcedure.input(boardChangeVisibilitySchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
if (
input.visibility !== "public" &&
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot make home board private",
});
}
if (
input.visibility !== "public" &&
(boardSettings.homeBoardId === input.id || boardSettings.mobileHomeBoardId === input.id)
) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot make home board private",
});
}
await ctx.db
.update(boards)
.set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id));
}),
await ctx.db
.update(boards)
.set({ isPublic: input.visibility === "public" })
.where(eq(boards.id, input.id));
}),
deleteBoard: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
@@ -572,13 +583,13 @@ export const boardRouter = createTRPCRouter({
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}),
getBoardByName: publicProcedure.input(validation.board.byName).query(async ({ input, ctx }) => {
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
}),
saveLayouts: protectedProcedure.input(validation.board.saveLayouts).mutation(async ({ ctx, input }) => {
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
@@ -704,7 +715,7 @@ export const boardRouter = createTRPCRouter({
}
}),
savePartialBoardSettings: protectedProcedure
.input(validation.board.savePartialSettings.and(z.object({ id: z.string() })))
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
@@ -738,7 +749,7 @@ export const boardRouter = createTRPCRouter({
})
.where(eq(boards.id, input.id));
}),
saveBoard: protectedProcedure.input(validation.board.save).mutation(async ({ input, ctx }) => {
saveBoard: protectedProcedure.input(boardSaveSchema).mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
@@ -1154,8 +1165,7 @@ export const boardRouter = createTRPCRouter({
},
});
}),
getBoardPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
getBoardPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
@@ -1226,92 +1236,86 @@ export const boardRouter = createTRPCRouter({
}),
};
}),
saveUserBoardPermissions: protectedProcedure
.input(validation.board.savePermissions)
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
saveUserBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction
.delete(schema.boardUserPermissions)
.where(eq(boardUserPermissions.boardId, input.entityId));
if (input.permissions.length === 0) {
return;
}
await transaction.insert(schema.boardUserPermissions).values(
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction.delete(schema.boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId));
if (input.permissions.length === 0) {
return;
}
await transaction.insert(schema.boardUserPermissions).values(
input.permissions.map((permission) => ({
userId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardUserPermissions)
.values(
input.permissions.map((permission) => ({
userId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardUserPermissions).where(eq(boardUserPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardUserPermissions)
.values(
input.permissions.map((permission) => ({
userId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
)
.run();
});
},
});
}),
saveGroupBoardPermissions: protectedProcedure
.input(validation.board.savePermissions)
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
)
.run();
});
},
});
}),
saveGroupBoardPermissions: protectedProcedure.input(boardSavePermissionsSchema).mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.entityId), "full");
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction
.delete(schema.boardGroupPermissions)
.where(eq(boardGroupPermissions.boardId, input.entityId));
if (input.permissions.length === 0) {
return;
}
await transaction.insert(schema.boardGroupPermissions).values(
await handleTransactionsAsync(ctx.db, {
async handleAsync(db, schema) {
await db.transaction(async (transaction) => {
await transaction
.delete(schema.boardGroupPermissions)
.where(eq(boardGroupPermissions.boardId, input.entityId));
if (input.permissions.length === 0) {
return;
}
await transaction.insert(schema.boardGroupPermissions).values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardGroupPermissions)
.values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
);
});
},
handleSync(db) {
db.transaction((transaction) => {
transaction.delete(boardGroupPermissions).where(eq(boardGroupPermissions.boardId, input.entityId)).run();
if (input.permissions.length === 0) {
return;
}
transaction
.insert(boardGroupPermissions)
.values(
input.permissions.map((permission) => ({
groupId: permission.principalId,
permission: permission.permission,
boardId: input.entityId,
})),
)
.run();
});
},
});
}),
)
.run();
});
},
});
}),
importOldmarrConfig: permissionRequiredProcedure
.requiresPermission("board-create")
.input(importJsonFileSchema)

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { zfd } from "zod-form-data";
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
import { superRefineCertificateFile, validation } from "@homarr/validation";
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -20,7 +20,7 @@ export const certificateRouter = createTRPCRouter({
}),
removeCertificate: permissionRequiredProcedure
.requiresPermission("admin")
.input(z.object({ fileName: validation.certificates.validFileNameSchema }))
.input(z.object({ fileName: certificateValidFileNameSchema }))
.mutation(async ({ input }) => {
await removeCustomRootCertificateAsync(input.fileName);
}),

View File

@@ -6,7 +6,15 @@ import { and, createId, eq, handleTransactionsAsync, like, not } from "@homarr/d
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups } from "@homarr/db/schema";
import { everyoneGroup } from "@homarr/definitions";
import { validation } from "@homarr/validation";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import {
groupCreateSchema,
groupSavePartialSettingsSchema,
groupSavePermissionsSchema,
groupSavePositionsSchema,
groupUpdateSchema,
groupUserSchema,
} from "@homarr/validation/group";
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, protectedProcedure } from "../trpc";
import { throwIfCredentialsDisabled } from "./invite/checks";
@@ -39,7 +47,7 @@ export const groupRouter = createTRPCRouter({
getPaginated: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.common.paginated)
.input(paginatedSchema)
.query(async ({ input, ctx }) => {
const whereQuery = input.search ? like(groups.name, `%${input.search.trim()}%`) : undefined;
const groupCount = await ctx.db.$count(groups, whereQuery);
@@ -74,7 +82,7 @@ export const groupRouter = createTRPCRouter({
}),
getById: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.common.byId)
.input(byIdSchema)
.query(async ({ input, ctx }) => {
const group = await ctx.db.query.groups.findFirst({
where: eq(groups.id, input.id),
@@ -169,7 +177,7 @@ export const groupRouter = createTRPCRouter({
}),
createInitialExternalGroup: onboardingProcedure
.requiresStep("group")
.input(validation.group.create)
.input(groupCreateSchema)
.mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -191,7 +199,7 @@ export const groupRouter = createTRPCRouter({
}),
createGroup: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.create)
.input(groupCreateSchema)
.mutation(async ({ input, ctx }) => {
await checkSimilarNameAndThrowAsync(ctx.db, input.name);
@@ -209,7 +217,7 @@ export const groupRouter = createTRPCRouter({
}),
updateGroup: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.update)
.input(groupUpdateSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -225,7 +233,7 @@ export const groupRouter = createTRPCRouter({
}),
savePartialSettings: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.savePartialSettings)
.input(groupSavePartialSettingsSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id);
@@ -239,7 +247,7 @@ export const groupRouter = createTRPCRouter({
}),
savePositions: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.savePositions)
.input(groupSavePositionsSchema)
.mutation(async ({ input, ctx }) => {
const positions = input.positions.map((id, index) => ({ id, position: index + 1 }));
@@ -262,7 +270,7 @@ export const groupRouter = createTRPCRouter({
}),
savePermissions: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.savePermissions)
.input(groupSavePermissionsSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
@@ -277,7 +285,7 @@ export const groupRouter = createTRPCRouter({
}),
transferOwnership: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.groupUser)
.input(groupUserSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -291,7 +299,7 @@ export const groupRouter = createTRPCRouter({
}),
deleteGroup: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.common.byId)
.input(byIdSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.id);
await throwIfGroupNameIsReservedAsync(ctx.db, input.id);
@@ -300,7 +308,7 @@ export const groupRouter = createTRPCRouter({
}),
addMember: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.groupUser)
.input(groupUserSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);
@@ -324,7 +332,7 @@ export const groupRouter = createTRPCRouter({
}),
removeMember: permissionRequiredProcedure
.requiresPermission("admin")
.input(validation.group.groupUser)
.input(groupUserSchema)
.mutation(async ({ input, ctx }) => {
await throwIfGroupNotFoundAsync(ctx.db, input.groupId);
await throwIfGroupNameIsReservedAsync(ctx.db, input.groupId);

View File

@@ -1,11 +1,11 @@
import { and, like } from "@homarr/db";
import { icons } from "@homarr/db/schema";
import { validation } from "@homarr/validation";
import { iconsFindSchema } from "@homarr/validation/icons";
import { createTRPCRouter, publicProcedure } from "../trpc";
export const iconsRouter = createTRPCRouter({
findIcons: publicProcedure.input(validation.icons.findIcons).query(async ({ ctx, input }) => {
findIcons: publicProcedure.input(iconsFindSchema).query(async ({ ctx, input }) => {
return {
icons: await ctx.db.query.iconRepositories.findMany({
with: {

View File

@@ -24,7 +24,12 @@ import {
integrationSecretKindObject,
} from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import { validation } from "@homarr/validation";
import { byIdSchema } from "@homarr/validation/common";
import {
integrationCreateSchema,
integrationSavePermissionsSchema,
integrationUpdateSchema,
} from "@homarr/validation/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
@@ -141,7 +146,7 @@ export const integrationRouter = createTRPCRouter({
},
});
}),
byId: protectedProcedure.input(validation.integration.byId).query(async ({ ctx, input }) => {
byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({
where: eq(integrations.id, input.id),
@@ -178,7 +183,7 @@ export const integrationRouter = createTRPCRouter({
}),
create: permissionRequiredProcedure
.requiresPermission("integration-create")
.input(validation.integration.create)
.input(integrationCreateSchema)
.mutation(async ({ ctx, input }) => {
await testConnectionAsync({
id: "new",
@@ -221,7 +226,7 @@ export const integrationRouter = createTRPCRouter({
});
}
}),
update: protectedProcedure.input(validation.integration.update).mutation(async ({ ctx, input }) => {
update: protectedProcedure.input(integrationUpdateSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({
@@ -282,7 +287,7 @@ export const integrationRouter = createTRPCRouter({
}
}
}),
delete: protectedProcedure.input(validation.integration.delete).mutation(async ({ ctx, input }) => {
delete: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const integration = await ctx.db.query.integrations.findFirst({
@@ -298,7 +303,7 @@ export const integrationRouter = createTRPCRouter({
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
}),
getIntegrationPermissions: protectedProcedure.input(validation.board.permissions).query(async ({ input, ctx }) => {
getIntegrationPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
const dbGroupPermissions = await ctx.db.query.groupPermissions.findMany({
@@ -370,7 +375,7 @@ export const integrationRouter = createTRPCRouter({
};
}),
saveUserIntegrationPermissions: protectedProcedure
.input(validation.integration.savePermissions)
.input(integrationSavePermissionsSchema)
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");
@@ -416,7 +421,7 @@ export const integrationRouter = createTRPCRouter({
});
}),
saveGroupIntegrationPermissions: protectedProcedure
.input(validation.integration.savePermissions)
.input(integrationSavePermissionsSchema)
.mutation(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.entityId), "full");

View File

@@ -1,16 +1,42 @@
import type { z } from "zod";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { validation } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../trpc";
const citySchema = z.object({
id: z.number(),
name: z.string(),
country: z.string().optional(),
country_code: z.string().optional(),
latitude: z.number(),
longitude: z.number(),
population: z.number().optional(),
});
export const locationSearchCityInput = z.object({
query: z.string(),
});
export const locationSearchCityOutput = z
.object({
results: z.array(citySchema),
})
.or(
z
.object({
generationtime_ms: z.number(),
})
.refine((data) => Object.keys(data).length === 1, { message: "Invalid response" })
.transform(() => ({ results: [] })), // We fallback to empty array if no results
);
export const locationRouter = createTRPCRouter({
searchCity: publicProcedure
.input(validation.location.searchCity.input)
.output(validation.location.searchCity.output)
.input(locationSearchCityInput)
.output(locationSearchCityOutput)
.query(async ({ input }) => {
const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
return (await res.json()) as z.infer<typeof validation.location.searchCity.output>;
return (await res.json()) as z.infer<typeof locationSearchCityOutput>;
}),
});

View File

@@ -5,14 +5,15 @@ import type { InferInsertModel } from "@homarr/db";
import { and, createId, desc, eq, like } from "@homarr/db";
import { iconRepositories, icons, medias } from "@homarr/db/schema";
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
import { validation } from "@homarr/validation";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import { mediaUploadSchema } from "@homarr/validation/media";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
export const mediaRouter = createTRPCRouter({
getPaginated: protectedProcedure
.input(
validation.common.paginated.and(
paginatedSchema.and(
z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }),
),
)
@@ -51,7 +52,7 @@ export const mediaRouter = createTRPCRouter({
}),
uploadMedia: permissionRequiredProcedure
.requiresPermission("media-upload")
.input(validation.media.uploadMedia)
.input(mediaUploadSchema)
.mutation(async ({ ctx, input }) => {
const content = Buffer.from(await input.file.arrayBuffer());
const id = createId();
@@ -82,7 +83,7 @@ export const mediaRouter = createTRPCRouter({
return id;
}),
deleteMedia: protectedProcedure.input(validation.common.byId).mutation(async ({ ctx, input }) => {
deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
const dbMedia = await ctx.db.query.medias.findFirst({
where: eq(medias.id, input.id),
columns: {

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { onboarding } from "@homarr/db/schema";
import { onboardingSteps } from "@homarr/definitions";
import { zodEnumFromArray } from "@homarr/validation";
import { zodEnumFromArray } from "@homarr/validation/enums";
import { createTRPCRouter, publicProcedure } from "../../trpc";
import { getOnboardingOrFallbackAsync, nextOnboardingStepAsync } from "./onboard-queries";

View File

@@ -5,13 +5,15 @@ import { asc, createId, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema";
import { createIntegrationAsync } from "@homarr/integrations";
import { validation } from "@homarr/validation";
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
export const searchEngineRouter = createTRPCRouter({
getPaginated: protectedProcedure.input(validation.common.paginated).query(async ({ input, ctx }) => {
getPaginated: protectedProcedure.input(paginatedSchema).query(async ({ input, ctx }) => {
const whereQuery = input.search ? like(searchEngines.name, `%${input.search.trim()}%`) : undefined;
const searchEngineCount = await ctx.db.$count(searchEngines, whereQuery);
@@ -41,7 +43,7 @@ export const searchEngineRouter = createTRPCRouter({
.then((engines) => engines.map((engine) => ({ value: engine.id, label: engine.name })));
}),
byId: protectedProcedure.input(validation.common.byId).query(async ({ ctx, input }) => {
byId: protectedProcedure.input(byIdSchema).query(async ({ ctx, input }) => {
const searchEngine = await ctx.db.query.searchEngines.findFirst({
where: eq(searchEngines.id, input.id),
});
@@ -115,7 +117,7 @@ export const searchEngineRouter = createTRPCRouter({
return null;
}),
search: protectedProcedure.input(validation.common.search).query(async ({ ctx, input }) => {
search: protectedProcedure.input(searchSchema).query(async ({ ctx, input }) => {
return await ctx.db.query.searchEngines.findMany({
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
with: {
@@ -132,21 +134,21 @@ export const searchEngineRouter = createTRPCRouter({
}),
getMediaRequestOptions: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
.input(validation.common.mediaRequestOptions)
.input(mediaRequestOptionsSchema)
.query(async ({ ctx, input }) => {
const integration = await createIntegrationAsync(ctx.integration);
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
}),
requestMedia: protectedProcedure
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
.input(validation.common.requestMedia)
.input(mediaRequestRequestSchema)
.mutation(async ({ ctx, input }) => {
const integration = await createIntegrationAsync(ctx.integration);
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
}),
create: permissionRequiredProcedure
.requiresPermission("search-engine-create")
.input(validation.searchEngine.manage)
.input(searchEngineManageSchema)
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(searchEngines).values({
id: createId(),
@@ -161,7 +163,7 @@ export const searchEngineRouter = createTRPCRouter({
}),
update: permissionRequiredProcedure
.requiresPermission("search-engine-modify-all")
.input(validation.searchEngine.edit)
.input(searchEngineEditSchema)
.mutation(async ({ ctx, input }) => {
const searchEngine = await ctx.db.query.searchEngines.findFirst({
where: eq(searchEngines.id, input.id),
@@ -188,7 +190,7 @@ export const searchEngineRouter = createTRPCRouter({
}),
delete: permissionRequiredProcedure
.requiresPermission("search-engine-full-all")
.input(validation.common.byId)
.input(byIdSchema)
.mutation(async ({ ctx, input }) => {
await ctx.db
.update(users)

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } from "@homarr/server-settings";
import { validation } from "@homarr/validation";
import { settingsInitSchema } from "@homarr/validation/settings";
import { createTRPCRouter, onboardingProcedure, permissionRequiredProcedure, publicProcedure } from "../trpc";
import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
@@ -32,7 +32,7 @@ export const serverSettingsRouter = createTRPCRouter({
}),
initSettings: onboardingProcedure
.requiresStep("settings")
.input(validation.settings.init)
.input(settingsInitSchema)
.mutation(async ({ ctx, input }) => {
await updateServerSettingByKeyAsync(ctx.db, "analytics", input.analytics);
await updateServerSettingByKeyAsync(ctx.db, "crawlingAndIndexing", input.crawlingAndIndexing);

View File

@@ -10,7 +10,20 @@ import { selectUserSchema } from "@homarr/db/validationSchemas";
import { credentialsAdminGroup } from "@homarr/definitions";
import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { validation } from "@homarr/validation";
import { byIdSchema } from "@homarr/validation/common";
import type { userBaseCreateSchema } from "@homarr/validation/user";
import {
userChangeColorSchemeSchema,
userChangeHomeBoardsSchema,
userChangePasswordApiSchema,
userChangeSearchPreferencesSchema,
userCreateSchema,
userEditProfileSchema,
userFirstDayOfWeekSchema,
userInitSchema,
userPingIconsEnabledSchema,
userRegistrationApiSchema,
} from "@homarr/validation/user";
import { convertIntersectionToZodObject } from "../schema-merger";
import {
@@ -28,7 +41,7 @@ import { changeSearchPreferencesAsync, changeSearchPreferencesInputSchema } from
export const userRouter = createTRPCRouter({
initUser: onboardingProcedure
.requiresStep("user")
.input(validation.user.init)
.input(userInitSchema)
.mutation(async ({ ctx, input }) => {
throwIfCredentialsDisabled();
@@ -52,7 +65,7 @@ export const userRouter = createTRPCRouter({
await nextOnboardingStepAsync(ctx.db, undefined);
}),
register: publicProcedure
.input(validation.user.registrationApi)
.input(userRegistrationApiSchema)
.output(z.void())
.mutation(async ({ ctx, input }) => {
throwIfCredentialsDisabled();
@@ -82,7 +95,7 @@ export const userRouter = createTRPCRouter({
create: permissionRequiredProcedure
.requiresPermission("admin")
.meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } })
.input(validation.user.create)
.input(userCreateSchema)
.output(z.void())
.mutation(async ({ ctx, input }) => {
throwIfCredentialsDisabled();
@@ -259,7 +272,7 @@ export const userRouter = createTRPCRouter({
return user;
}),
editProfile: protectedProcedure
.input(validation.user.editProfile)
.input(userEditProfileSchema)
.output(z.void())
.meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => {
@@ -318,7 +331,7 @@ export const userRouter = createTRPCRouter({
await ctx.db.delete(users).where(eq(users.id, input.userId));
}),
changePassword: protectedProcedure
.input(validation.user.changePasswordApi)
.input(userChangePasswordApiSchema)
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/{userId}/changePassword", tags: ["users"], protect: true } })
.mutation(async ({ ctx, input }) => {
@@ -384,7 +397,7 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, input.userId));
}),
changeHomeBoards: protectedProcedure
.input(convertIntersectionToZodObject(validation.user.changeHomeBoards.and(z.object({ userId: z.string() }))))
.input(convertIntersectionToZodObject(userChangeHomeBoardsSchema.and(z.object({ userId: z.string() }))))
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => {
@@ -430,7 +443,7 @@ export const userRouter = createTRPCRouter({
changeDefaultSearchEngine: protectedProcedure
.input(
convertIntersectionToZodObject(
validation.user.changeSearchPreferences.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
userChangeSearchPreferencesSchema.omit({ openInNewTab: true }).and(z.object({ userId: z.string() })),
),
)
.output(z.void())
@@ -457,7 +470,7 @@ export const userRouter = createTRPCRouter({
await changeSearchPreferencesAsync(ctx.db, ctx.session, input);
}),
changeColorScheme: protectedProcedure
.input(validation.user.changeColorScheme)
.input(userChangeColorSchemeSchema)
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => {
@@ -469,7 +482,7 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, ctx.session.user.id));
}),
changePingIconsEnabled: protectedProcedure
.input(validation.user.pingIconsEnabled.and(validation.common.byId))
.input(userPingIconsEnabledSchema.and(byIdSchema))
.mutation(async ({ input, ctx }) => {
// Only admins can change other users ping icons enabled
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
@@ -487,7 +500,7 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, ctx.session.user.id));
}),
changeFirstDayOfWeek: protectedProcedure
.input(convertIntersectionToZodObject(validation.user.firstDayOfWeek.and(validation.common.byId)))
.input(convertIntersectionToZodObject(userFirstDayOfWeekSchema.and(byIdSchema)))
.output(z.void())
.meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } })
.mutation(async ({ input, ctx }) => {
@@ -522,7 +535,7 @@ export const userRouter = createTRPCRouter({
}),
});
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof validation.user.baseCreate>, "groupIds">) => {
const createUserAsync = async (db: Database, input: Omit<z.infer<typeof userBaseCreateSchema>, "groupIds">) => {
const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt);

View File

@@ -6,9 +6,9 @@ import type { Modify } from "@homarr/common/types";
import { eq } from "@homarr/db";
import type { Database } from "@homarr/db";
import { users } from "@homarr/db/schema";
import { validation } from "@homarr/validation";
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
export const changeSearchPreferencesInputSchema = validation.user.changeSearchPreferences.and(
export const changeSearchPreferencesInputSchema = userChangeSearchPreferencesSchema.and(
z.object({ userId: z.string() }),
);

View File

@@ -1,6 +1,6 @@
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding";
import { validation } from "@homarr/validation";
import { paginatedSchema } from "@homarr/validation/common";
import type { IntegrationAction } from "../../middlewares/integration";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
@@ -12,7 +12,7 @@ const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
export const mediaTranscodingRouter = createTRPCRouter({
getDataAsync: publicProcedure
.unstable_concat(createIndexerManagerIntegrationMiddleware("query"))
.input(validation.common.paginated.pick({ page: true, pageSize: true }))
.input(paginatedSchema.pick({ page: true, pageSize: true }))
.query(async ({ ctx, input }) => {
const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, {
pageOffset: input.page,

View File

@@ -1,15 +1,39 @@
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { validation } from "@homarr/validation";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const atLocationInput = z.object({
longitude: z.number(),
latitude: z.number(),
});
const atLocationOutput = z.object({
current_weather: z.object({
weathercode: z.number(),
temperature: z.number(),
windspeed: z.number(),
}),
daily: z.object({
time: z.array(z.string()),
weathercode: z.array(z.number()),
temperature_2m_max: z.array(z.number()),
temperature_2m_min: z.array(z.number()),
sunrise: z.array(z.string()),
sunset: z.array(z.string()),
wind_speed_10m_max: z.array(z.number()),
wind_gusts_10m_max: z.array(z.number()),
}),
});
export const weatherRouter = createTRPCRouter({
atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => {
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
const res = await fetchWithTimeout(
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
);
const json: unknown = await res.json();
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json);
const weather = await atLocationOutput.parseAsync(json);
return {
current: weather.current_weather,
daily: weather.daily.time.map((value, index) => {

View File

@@ -5,11 +5,11 @@ import type { Database } from "@homarr/db";
import { and, eq } from "@homarr/db";
import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { validation } from "@homarr/validation";
import type { userSignInSchema } from "@homarr/validation/user";
export const authorizeWithBasicCredentialsAsync = async (
db: Database,
credentials: z.infer<typeof validation.user.signIn>,
credentials: z.infer<typeof userSignInSchema>,
) => {
const user = await db.query.users.findFirst({
where: and(eq(users.name, credentials.name.toLowerCase()), eq(users.provider, "credentials")),

View File

@@ -6,14 +6,14 @@ import type { Database, InferInsertModel } from "@homarr/db";
import { and, createId, eq } from "@homarr/db";
import { users } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import type { validation } from "@homarr/validation";
import type { userSignInSchema } from "@homarr/validation/user";
import { env } from "../../../env";
import { LdapClient } from "../ldap-client";
export const authorizeWithLdapCredentialsAsync = async (
db: Database,
credentials: z.infer<typeof validation.user.signIn>,
credentials: z.infer<typeof userSignInSchema>,
) => {
logger.info(`user ${credentials.name} is trying to log in using LDAP. Connecting to LDAP server...`);
const client = new LdapClient();

View File

@@ -1,7 +1,7 @@
import type Credentials from "@auth/core/providers/credentials";
import type { Database } from "@homarr/db";
import { validation } from "@homarr/validation";
import { userSignInSchema } from "@homarr/validation/user";
import { authorizeWithBasicCredentialsAsync } from "./authorization/basic-authorization";
import { authorizeWithLdapCredentialsAsync } from "./authorization/ldap-authorization";
@@ -15,7 +15,7 @@ export const createCredentialsConfiguration = (db: Database) =>
name: "Credentials",
// eslint-disable-next-line no-restricted-syntax
async authorize(credentials) {
const data = await validation.user.signIn.parseAsync(credentials);
const data = await userSignInSchema.parseAsync(credentials);
return await authorizeWithBasicCredentialsAsync(db, data);
},
@@ -28,7 +28,7 @@ export const createLdapConfiguration = (db: Database) =>
name: "Ldap",
// eslint-disable-next-line no-restricted-syntax
async authorize(credentials) {
const data = await validation.user.signIn.parseAsync(credentials);
const data = await userSignInSchema.parseAsync(credentials);
return await authorizeWithLdapCredentialsAsync(db, data).catch(() => null);
},
}) satisfies CredentialsConfiguration;

View File

@@ -5,7 +5,7 @@ import { generateSecureRandomToken } from "@homarr/common/server";
import { and, count, createId, db, eq } from "@homarr/db";
import { getMaxGroupPositionAsync } from "@homarr/db/queries";
import { groupMembers, groupPermissions, groups, users } from "@homarr/db/schema";
import { usernameSchema } from "@homarr/validation";
import { usernameSchema } from "@homarr/validation/user";
export const recreateAdmin = command({
name: "recreate-admin",

View File

@@ -1,7 +1,7 @@
import { objectKeys } from "@homarr/common";
import type { JobGroupKeys } from "@homarr/cron-jobs";
import { createSubPubChannel } from "@homarr/redis";
import { zodEnumFromArray } from "@homarr/validation";
import { zodEnumFromArray } from "@homarr/validation/enums";
export const cronJobRunnerChannel = createSubPubChannel<JobGroupKeys>("cron-job-runner", { persist: false });

View File

@@ -3,7 +3,7 @@ import { z } from "zod";
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod";
import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form";
import { zodErrorMap } from "@homarr/validation/form/i18n";
export const useZodForm = <
TSchema extends

View File

@@ -8,7 +8,7 @@ import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation";
import type { appManageSchema } from "@homarr/validation/app";
import { AppForm } from "./_form";
@@ -33,7 +33,7 @@ export const AppNewForm = ({
});
const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>, redirect: boolean, afterSuccess?: () => void) => {
(values: z.infer<typeof appManageSchema>, redirect: boolean, afterSuccess?: () => void) => {
mutate(values, {
onSuccess() {
showSuccessNotification({

View File

@@ -10,12 +10,12 @@ import type { z } from "zod";
import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { appManageSchema } from "@homarr/validation/app";
import { IconPicker } from "../icon-picker/icon-picker";
import { findBestIconMatch } from "./icon-matcher";
type FormType = z.infer<typeof validation.app.manage>;
type FormType = z.infer<typeof appManageSchema>;
interface AppFormProps {
showBackToOverview: boolean;
@@ -37,7 +37,7 @@ export const AppForm = ({
}: AppFormProps) => {
const t = useI18n();
const form = useZodForm(validation.app.manage, {
const form = useZodForm(appManageSchema, {
initialValues: {
name: initialValues?.name ?? "",
description: initialValues?.description ?? "",

View File

@@ -5,7 +5,7 @@ import { clientApi } from "@homarr/api/client";
import type { MaybePromise } from "@homarr/common/types";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { supportedMediaUploadFormats } from "@homarr/validation";
import { supportedMediaUploadFormats } from "@homarr/validation/media";
interface UploadMediaProps {
children: (props: { onClick: () => void; loading: boolean }) => JSX.Element;

View File

@@ -5,7 +5,7 @@ import { AppForm } from "@homarr/forms-collection";
import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation } from "@homarr/validation";
import type { appManageSchema } from "@homarr/validation/app";
interface QuickAddAppModalProps {
onClose: (createdAppId: string) => Promise<void>;
@@ -24,7 +24,7 @@ export const QuickAddAppModal = createModal<QuickAddAppModalProps>(({ actions, i
},
});
const handleSubmit = (values: z.infer<typeof validation.app.manage>) => {
const handleSubmit = (values: z.infer<typeof appManageSchema>) => {
mutate(values, {
async onSuccess({ appId }) {
showSuccessNotification({

View File

@@ -8,11 +8,11 @@ import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
export const AddBoardModal = createModal(({ actions }) => {
const t = useI18n();
const form = useZodForm(validation.board.create, {
const form = useZodForm(boardCreateSchema, {
mode: "controlled",
initialValues: {
name: "",
@@ -28,7 +28,7 @@ export const AddBoardModal = createModal(({ actions }) => {
const boardNameStatus = useBoardNameStatus(form.values.name);
const columnCountChecks = validation.board.create.shape.columnCount._def.checks;
const columnCountChecks = boardColumnCountSchema._def.checks;
const minColumnCount = columnCountChecks.find((check) => check.kind === "min")?.value;
const maxColumnCount = columnCountChecks.find((check) => check.kind === "max")?.value;
@@ -97,7 +97,7 @@ export const useBoardNameStatus = (name: string) => {
const t = useI18n();
const [debouncedName] = useDebouncedValue(name, 250);
const { data: boardExists, isLoading } = clientApi.board.exists.useQuery(debouncedName, {
enabled: validation.board.create.shape.name.safeParse(debouncedName).success,
enabled: boardNameSchema.safeParse(debouncedName).success,
});
return {

View File

@@ -5,7 +5,7 @@ import type { MaybePromise } from "@homarr/common/types";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { boardDuplicateSchema } from "@homarr/validation/board";
import { createModal } from "../../../modals/src/creator";
import { useBoardNameStatus } from "./add-board-modal";
@@ -20,7 +20,7 @@ interface InnerProps {
export const DuplicateBoardModal = createModal<InnerProps>(({ actions, innerProps }) => {
const t = useI18n();
const form = useZodForm(validation.board.duplicate.omit({ id: true }), {
const form = useZodForm(boardDuplicateSchema.omit({ id: true }), {
mode: "controlled",
initialValues: {
name: innerProps.board.name,

View File

@@ -6,12 +6,12 @@ import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { groupCreateSchema } from "@homarr/validation/group";
export const AddGroupModal = createModal<void>(({ actions }) => {
const t = useI18n();
const { mutate, isPending } = clientApi.group.createGroup.useMutation();
const form = useZodForm(validation.group.create, {
const form = useZodForm(groupCreateSchema, {
initialValues: {
name: "",
},

View File

@@ -1,15 +1,15 @@
import { z } from "zod";
import { zfd } from "zod-form-data";
import { validation } from "@homarr/validation";
import { createCustomErrorParams } from "@homarr/validation/form";
import { boardNameSchema } from "@homarr/validation/board";
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
export const sidebarBehaviours = ["remove-items", "last-section"] as const;
export const defaultSidebarBehaviour = "last-section";
export type SidebarBehaviour = (typeof sidebarBehaviours)[number];
export const oldmarrImportConfigurationSchema = z.object({
name: validation.board.name,
name: boardNameSchema,
onlyImportApps: z.boolean().default(false),
sidebarBehaviour: z.enum(sidebarBehaviours).default(defaultSidebarBehaviour),
});

View File

@@ -3,7 +3,7 @@ import { useState } from "react";
import { Popover, Progress } from "@mantine/core";
import { useScopedI18n } from "@homarr/translation/client";
import { passwordRequirements } from "@homarr/validation";
import { passwordRequirements } from "@homarr/validation/user";
import { PasswordRequirement } from "./password-requirement";

View File

@@ -1 +0,0 @@
export * from "./src";

View File

@@ -5,8 +5,7 @@
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./form": "./src/form/i18n.ts"
"./*": "./src/*.ts"
},
"typesVersions": {
"*": {

View File

@@ -1,6 +1,15 @@
import { z } from "zod";
const manageAppSchema = z.object({
export const appHrefSchema = z
.string()
.trim()
.url()
.regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed)
.or(z.literal(""))
.transform((value) => (value.length === 0 ? null : value))
.nullable();
export const appManageSchema = z.object({
name: z.string().trim().min(1).max(64),
description: z
.string()
@@ -9,14 +18,7 @@ const manageAppSchema = z.object({
.transform((value) => (value.length === 0 ? null : value))
.nullable(),
iconUrl: z.string().trim().min(1),
href: z
.string()
.trim()
.url()
.regex(/^https?:\/\//) // Only allow http and https for security reasons (javascript: is not allowed)
.or(z.literal(""))
.transform((value) => (value.length === 0 ? null : value))
.nullable(),
href: appHrefSchema,
pingUrl: z
.string()
.trim()
@@ -27,12 +29,8 @@ const manageAppSchema = z.object({
.nullable(),
});
const editAppSchema = manageAppSchema.and(z.object({ id: z.string() }));
export const appCreateManySchema = z
.array(appManageSchema.omit({ iconUrl: true }).and(z.object({ iconUrl: z.string().min(1).nullable() })))
.min(1);
export const appSchemas = {
manage: manageAppSchema,
createMany: z
.array(manageAppSchema.omit({ iconUrl: true }).and(z.object({ iconUrl: z.string().min(1).nullable() })))
.min(1),
edit: editAppSchema,
};
export const appEditSchema = appManageSchema.and(z.object({ id: z.string() }));

View File

@@ -18,27 +18,28 @@ const hexColorNullableSchema = hexColorSchema
.nullable()
.transform((value) => (value?.trim().length === 0 ? null : value));
const boardNameSchema = z
export const boardNameSchema = z
.string()
.min(1)
.max(255)
.regex(/^[A-Za-z0-9-\\_]*$/);
export const boardColumnCountSchema = z.number().min(1).max(24);
const byNameSchema = z.object({
export const boardByNameSchema = z.object({
name: boardNameSchema,
});
const renameSchema = z.object({
export const boardRenameSchema = z.object({
id: z.string(),
name: boardNameSchema,
});
const duplicateSchema = z.object({
export const boardDuplicateSchema = z.object({
id: z.string(),
name: boardNameSchema,
});
const changeVisibilitySchema = z.object({
export const boardChangeVisibilitySchema = z.object({
id: z.string(),
visibility: z.enum(["public", "private"]),
});
@@ -48,7 +49,7 @@ const trimmedNullableString = z
.nullable()
.transform((value) => (value?.trim().length === 0 ? null : value));
const savePartialSettingsSchema = z
export const boardSavePartialSettingsSchema = z
.object({
pageTitle: trimmedNullableString,
metaTitle: trimmedNullableString,
@@ -68,52 +69,28 @@ const savePartialSettingsSchema = z
})
.partial();
const saveLayoutsSchema = z.object({
export const boardSaveLayoutsSchema = z.object({
id: z.string(),
layouts: z.array(
z.object({
id: z.string(),
name: z.string().trim().nonempty().max(32),
columnCount: z.number().min(1).max(24),
columnCount: boardColumnCountSchema,
breakpoint: z.number().min(0).max(32767),
}),
),
});
const saveSchema = z.object({
export const boardSaveSchema = z.object({
id: z.string(),
sections: z.array(sectionSchema),
items: z.array(commonItemSchema),
});
const createSchema = z.object({ name: boardNameSchema, columnCount: z.number().min(1).max(24), isPublic: z.boolean() });
const permissionsSchema = z.object({
id: z.string(),
});
const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions));
z.object({
entityId: z.string(),
permissions: z.array(
z.object({
principalId: z.string(),
permission: zodEnumFromArray(boardPermissions),
}),
),
});
export const boardSchemas = {
export const boardCreateSchema = z.object({
name: boardNameSchema,
byName: byNameSchema,
savePartialSettings: savePartialSettingsSchema,
saveLayouts: saveLayoutsSchema,
save: saveSchema,
create: createSchema,
duplicate: duplicateSchema,
rename: renameSchema,
changeVisibility: changeVisibilitySchema,
permissions: permissionsSchema,
savePermissions: savePermissionsSchema,
};
columnCount: boardColumnCountSchema,
isPublic: z.boolean(),
});
export const boardSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(boardPermissions));

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
import { createCustomErrorParams } from "./form/i18n";
const validFileNameSchema = z.string().regex(/^[\w\-. ]+$/);
export const certificateValidFileNameSchema = z.string().regex(/^[\w\-. ]+$/);
export const superRefineCertificateFile = (value: File | null, context: z.RefinementCtx) => {
if (!value) {
@@ -13,7 +13,7 @@ export const superRefineCertificateFile = (value: File | null, context: z.Refine
});
}
const result = validFileNameSchema.safeParse(value.name);
const result = certificateValidFileNameSchema.safeParse(value.name);
if (!result.success) {
return context.addIssue({
code: "custom",
@@ -46,7 +46,3 @@ export const superRefineCertificateFile = (value: File | null, context: z.Refine
return null;
};
export const certificateSchemas = {
validFileNameSchema,
};

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
const paginatedSchema = z.object({
export const paginatedSchema = z.object({
search: z.string().optional(),
pageSize: z.number().int().positive().default(10),
page: z.number().int().positive().default(1),
@@ -10,26 +10,7 @@ export const byIdSchema = z.object({
id: z.string(),
});
const searchSchema = z.object({
export const searchSchema = z.object({
query: z.string(),
limit: z.number().int().positive().default(10),
});
const mediaRequestOptionsSchema = z.object({
mediaId: z.number(),
mediaType: z.enum(["tv", "movie"]),
});
const requestMediaSchema = z.object({
mediaType: z.enum(["tv", "movie"]),
mediaId: z.number(),
seasons: z.array(z.number().min(0)).optional(),
});
export const commonSchemas = {
paginated: paginatedSchema,
byId: byIdSchema,
search: searchSchema,
mediaRequestOptions: mediaRequestOptionsSchema,
requestMedia: requestMediaSchema,
};

View File

@@ -5,7 +5,7 @@ import { everyoneGroup, groupPermissionKeys } from "@homarr/definitions";
import { byIdSchema } from "./common";
import { zodEnumFromArray } from "./enums";
const createSchema = z.object({
export const groupCreateSchema = z.object({
name: z
.string()
.trim()
@@ -16,35 +16,25 @@ const createSchema = z.object({
}),
});
const updateSchema = createSchema.merge(byIdSchema);
export const groupUpdateSchema = groupCreateSchema.merge(byIdSchema);
const settingsSchema = z.object({
export const groupSettingsSchema = z.object({
homeBoardId: z.string().nullable(),
mobileHomeBoardId: z.string().nullable(),
});
const savePartialSettingsSchema = z.object({
export const groupSavePartialSettingsSchema = z.object({
id: z.string(),
settings: settingsSchema.partial(),
settings: groupSettingsSchema.partial(),
});
const savePermissionsSchema = z.object({
export const groupSavePermissionsSchema = z.object({
groupId: z.string(),
permissions: z.array(zodEnumFromArray(groupPermissionKeys)),
});
const savePositionsSchema = z.object({
export const groupSavePositionsSchema = z.object({
positions: z.array(z.string()),
});
const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() });
export const groupSchemas = {
create: createSchema,
update: updateSchema,
savePermissions: savePermissionsSchema,
groupUser: groupUserSchema,
savePartialSettings: savePartialSettingsSchema,
settings: settingsSchema,
savePositions: savePositionsSchema,
};
export const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() });

View File

@@ -1,10 +1,6 @@
import { z } from "zod";
const findIconsSchema = z.object({
export const iconsFindSchema = z.object({
searchText: z.string().optional(),
limitPerGroup: z.number().min(1).max(500).default(12),
});
export const iconsSchemas = {
findIcons: findIconsSchema,
};

View File

@@ -1,42 +0,0 @@
import { appSchemas } from "./app";
import { boardSchemas } from "./board";
import { certificateSchemas } from "./certificates";
import { commonSchemas } from "./common";
import { groupSchemas } from "./group";
import { iconsSchemas } from "./icons";
import { integrationSchemas } from "./integration";
import { locationSchemas } from "./location";
import { mediaSchemas } from "./media";
import { searchEngineSchemas } from "./search-engine";
import { settingsSchemas } from "./settings";
import { userSchemas } from "./user";
import { widgetSchemas } from "./widgets";
export const validation = {
user: userSchemas,
group: groupSchemas,
integration: integrationSchemas,
board: boardSchemas,
app: appSchemas,
widget: widgetSchemas,
location: locationSchemas,
icons: iconsSchemas,
searchEngine: searchEngineSchemas,
media: mediaSchemas,
settings: settingsSchemas,
common: commonSchemas,
certificates: certificateSchemas,
};
export {
sectionSchema,
itemAdvancedOptionsSchema,
sharedItemSchema,
dynamicSectionOptionsSchema,
type BoardItemAdvancedOptions,
type BoardItemIntegration,
} from "./shared";
export { superRefineCertificateFile } from "./certificates";
export { passwordRequirements, usernameSchema } from "./user";
export { supportedMediaUploadFormats } from "./media";
export { zodEnumFromArray, zodUnionFromArray } from "./enums";

View File

@@ -5,7 +5,7 @@ import { integrationKinds, integrationPermissions, integrationSecretKinds } from
import { zodEnumFromArray } from "./enums";
import { createSavePermissionsSchema } from "./permissions";
const integrationCreateSchema = z.object({
export const integrationCreateSchema = z.object({
name: z.string().nonempty().max(127),
url: z
.string()
@@ -21,7 +21,7 @@ const integrationCreateSchema = z.object({
attemptSearchEngineCreation: z.boolean(),
});
const integrationUpdateSchema = z.object({
export const integrationUpdateSchema = z.object({
id: z.string().cuid2(),
name: z.string().nonempty().max(127),
url: z.string().url(),
@@ -33,29 +33,4 @@ const integrationUpdateSchema = z.object({
),
});
const idSchema = z.object({
id: z.string(),
});
const testConnectionSchema = z.object({
id: z.string().cuid2().nullable(), // Is used to use existing secrets if they have not been updated
url: z.string().url(),
kind: zodEnumFromArray(integrationKinds),
secrets: z.array(
z.object({
kind: zodEnumFromArray(integrationSecretKinds),
value: z.string().nullable(),
}),
),
});
const savePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(integrationPermissions));
export const integrationSchemas = {
create: integrationCreateSchema,
update: integrationUpdateSchema,
delete: idSchema,
byId: idSchema,
testConnection: testConnectionSchema,
savePermissions: savePermissionsSchema,
};
export const integrationSavePermissionsSchema = createSavePermissionsSchema(zodEnumFromArray(integrationPermissions));

View File

@@ -1,35 +0,0 @@
import { z } from "zod";
const citySchema = z.object({
id: z.number(),
name: z.string(),
country: z.string().optional(),
country_code: z.string().optional(),
latitude: z.number(),
longitude: z.number(),
population: z.number().optional(),
});
const searchCityInput = z.object({
query: z.string(),
});
const searchCityOutput = z
.object({
results: z.array(citySchema),
})
.or(
z
.object({
generationtime_ms: z.number(),
})
.refine((data) => Object.keys(data).length === 1, { message: "Invalid response" })
.transform(() => ({ results: [] })), // We fallback to empty array if no results
);
export const locationSchemas = {
searchCity: {
input: searchCityInput,
output: searchCityOutput,
},
};

View File

@@ -5,7 +5,7 @@ import { createCustomErrorParams } from "./form/i18n";
export const supportedMediaUploadFormats = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"];
export const uploadMediaSchema = zfd.formData({
export const mediaUploadSchema = zfd.formData({
file: zfd.file().superRefine((value: File | null, context: z.RefinementCtx) => {
if (!value) {
return context.addIssue({
@@ -38,7 +38,3 @@ export const uploadMediaSchema = zfd.formData({
return null;
}),
});
export const mediaSchemas = {
uploadMedia: uploadMediaSchema,
};

View File

@@ -13,7 +13,7 @@ const fromIntegrationSearchEngine = z.object({
integrationId: z.string().optional(),
});
const manageSearchEngineSchema = z.object({
const baseSearchEngineManageSchema = z.object({
name: z.string().min(1).max(64),
short: z.string().min(1).max(8),
iconUrl: z.string().min(1),
@@ -21,21 +21,18 @@ const manageSearchEngineSchema = z.object({
});
const createManageSearchEngineSchema = <T extends ZodTypeAny>(
callback: (schema: typeof manageSearchEngineSchema) => T,
callback: (schema: typeof baseSearchEngineManageSchema) => T,
) =>
z
.discriminatedUnion("type", [genericSearchEngine, fromIntegrationSearchEngine])
.and(callback(manageSearchEngineSchema));
.and(callback(baseSearchEngineManageSchema));
const editSearchEngineSchema = createManageSearchEngineSchema((schema) =>
export const searchEngineManageSchema = createManageSearchEngineSchema((schema) => schema);
export const searchEngineEditSchema = createManageSearchEngineSchema((schema) =>
schema
.extend({
id: z.string(),
})
.omit({ short: true }),
);
export const searchEngineSchemas = {
manage: createManageSearchEngineSchema((schema) => schema),
edit: editSearchEngineSchema,
};

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
const initSettingsSchema = z.object({
export const settingsInitSchema = z.object({
analytics: z.object({
enableGeneral: z.boolean(),
enableWidgetData: z.boolean(),
@@ -14,7 +14,3 @@ const initSettingsSchema = z.object({
noSiteLinksSearchBox: z.boolean(),
}),
});
export const settingsSchemas = {
init: initSettingsSchema,
};

View File

@@ -22,7 +22,7 @@ export const passwordRequirements = [
value: keyof TranslationObject["user"]["field"]["password"]["requirement"];
}[];
const passwordSchema = z
export const userPasswordSchema = z
.string()
.min(8)
.max(255)
@@ -51,39 +51,39 @@ const addConfirmPasswordRefinement = <TObj extends { password: string; confirmPa
});
};
const baseCreateUserSchema = z.object({
export const userBaseCreateSchema = z.object({
username: usernameSchema,
password: passwordSchema,
password: userPasswordSchema,
confirmPassword: z.string(),
email: z.string().email().or(z.string().length(0).optional()),
groupIds: z.array(z.string()),
});
const createUserSchema = addConfirmPasswordRefinement(baseCreateUserSchema);
export const userCreateSchema = addConfirmPasswordRefinement(userBaseCreateSchema);
const initUserSchema = addConfirmPasswordRefinement(baseCreateUserSchema.omit({ groupIds: true }));
export const userInitSchema = addConfirmPasswordRefinement(userBaseCreateSchema.omit({ groupIds: true }));
const signInSchema = z.object({
export const userSignInSchema = z.object({
name: z.string().min(1),
password: z.string().min(1),
});
const registrationSchema = addConfirmPasswordRefinement(
export const userRegistrationSchema = addConfirmPasswordRefinement(
z.object({
username: usernameSchema,
password: passwordSchema,
password: userPasswordSchema,
confirmPassword: z.string(),
}),
);
const registrationSchemaApi = registrationSchema.and(
export const userRegistrationApiSchema = userRegistrationSchema.and(
z.object({
inviteId: z.string(),
token: z.string(),
}),
);
const editProfileSchema = z.object({
export const userEditProfileSchema = z.object({
id: z.string(),
name: usernameSchema,
email: z
@@ -97,51 +97,33 @@ const editProfileSchema = z.object({
const baseChangePasswordSchema = z.object({
previousPassword: z.string().min(1),
password: passwordSchema,
password: userPasswordSchema,
confirmPassword: z.string(),
userId: z.string(),
});
const changePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema.omit({ userId: true }));
export const userChangePasswordSchema = addConfirmPasswordRefinement(baseChangePasswordSchema.omit({ userId: true }));
const changePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema);
export const userChangePasswordApiSchema = addConfirmPasswordRefinement(baseChangePasswordSchema);
const changeHomeBoardSchema = z.object({
export const userChangeHomeBoardsSchema = z.object({
homeBoardId: z.string().nullable(),
mobileHomeBoardId: z.string().nullable(),
});
const changeSearchPreferencesSchema = z.object({
export const userChangeSearchPreferencesSchema = z.object({
defaultSearchEngineId: z.string().min(1).nullable(),
openInNewTab: z.boolean(),
});
const changeColorSchemeSchema = z.object({
export const userChangeColorSchemeSchema = z.object({
colorScheme: zodEnumFromArray(colorSchemes),
});
const firstDayOfWeekSchema = z.object({
export const userFirstDayOfWeekSchema = z.object({
firstDayOfWeek: z.custom<DayOfWeek>((value) => z.number().min(0).max(6).safeParse(value).success),
});
const pingIconsEnabledSchema = z.object({
export const userPingIconsEnabledSchema = z.object({
pingIconsEnabled: z.boolean(),
});
export const userSchemas = {
signIn: signInSchema,
registration: registrationSchema,
registrationApi: registrationSchemaApi,
init: initUserSchema,
create: createUserSchema,
baseCreate: baseCreateUserSchema,
password: passwordSchema,
editProfile: editProfileSchema,
changePassword: changePasswordSchema,
changeHomeBoards: changeHomeBoardSchema,
changeSearchPreferences: changeSearchPreferencesSchema,
changePasswordApi: changePasswordApiSchema,
changeColorScheme: changeColorSchemeSchema,
firstDayOfWeek: firstDayOfWeekSchema,
pingIconsEnabled: pingIconsEnabledSchema,
};

View File

@@ -1,5 +0,0 @@
import { weatherWidgetSchemas } from "./weather";
export const widgetSchemas = {
weather: weatherWidgetSchemas,
};

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
export const mediaRequestOptionsSchema = z.object({
mediaId: z.number(),
mediaType: z.enum(["tv", "movie"]),
});
export const mediaRequestRequestSchema = z.object({
mediaType: z.enum(["tv", "movie"]),
mediaId: z.number(),
seasons: z.array(z.number().min(0)).optional(),
});

View File

@@ -1,29 +0,0 @@
import { z } from "zod";
export const atLocationInput = z.object({
longitude: z.number(),
latitude: z.number(),
});
export const atLocationOutput = z.object({
current_weather: z.object({
weathercode: z.number(),
temperature: z.number(),
windspeed: z.number(),
}),
daily: z.object({
time: z.array(z.string()),
weathercode: z.array(z.number()),
temperature_2m_max: z.array(z.number()),
temperature_2m_min: z.array(z.number()),
sunrise: z.array(z.string()),
sunset: z.array(z.string()),
wind_speed_10m_max: z.array(z.number()),
wind_gusts_10m_max: z.array(z.number()),
}),
});
export const weatherWidgetSchemas = {
atLocationInput,
atLocationOutput,
};

View File

@@ -6,7 +6,7 @@ import { useForm } from "@homarr/form";
import { createModal } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { TextMultiSelect } from "@homarr/ui";
import type { BoardItemAdvancedOptions } from "@homarr/validation";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
interface InnerProps {
advancedOptions: BoardItemAdvancedOptions;

View File

@@ -10,7 +10,7 @@ import { zodResolver } from "@homarr/form";
import { createModal, useModalAction } from "@homarr/modals";
import type { SettingsContextProps } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form";
import { zodErrorMap } from "@homarr/validation/form/i18n";
import { widgetImports } from "..";
import { getInputForType } from "../_inputs";