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

@@ -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,
};