Merge pull request #3415 from bluewave-labs/fix/unset-with-explicit-null
fix: unset with explicit null
This commit is contained in:
8 files changed
+42
-55
No files matched your search
@@ -1,14 +1,14 @@
|
||||
import { useMemo } from "react";
|
||||
import { settingsSchema } from "@/Validation/settings";
|
||||
import type { Settings } from "@/Types/Settings";
|
||||
import type { SettingsFormData } from "@/Validation/settings";
|
||||
import type { SettingsFormInput } from "@/Validation/settings";
|
||||
|
||||
interface UseSettingsFormOptions {
|
||||
data?: Settings | null;
|
||||
}
|
||||
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
|
||||
return useMemo(() => {
|
||||
const defaults: SettingsFormData = {
|
||||
const defaults: SettingsFormInput = {
|
||||
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
|
||||
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
|
||||
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
||||
import { useToast } from "@/Hooks/UseToast";
|
||||
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import type { SettingsFormData } from "@/Validation/settings";
|
||||
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings";
|
||||
import { useState } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
||||
@@ -89,7 +89,7 @@ export const SettingsPage = () => {
|
||||
// Initialize form with schema and defaults
|
||||
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
|
||||
|
||||
const form = useForm<SettingsFormData>({
|
||||
const form = useForm<SettingsFormInput, unknown, SettingsFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
mode: "onChange",
|
||||
@@ -256,20 +256,7 @@ export const SettingsPage = () => {
|
||||
delete (dataToSend as any).systemEmailPassword;
|
||||
}
|
||||
|
||||
// Convert undefined to empty string for backend unsetting
|
||||
const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => {
|
||||
const typedKey = key as keyof SettingsFormData;
|
||||
if (value === undefined) {
|
||||
(acc as any)[typedKey] = "";
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
(acc as any)[typedKey] = value;
|
||||
} else {
|
||||
(acc as any)[typedKey] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Partial<SettingsFormData>);
|
||||
|
||||
const result = await patch("/settings", processedData as SettingsFormData);
|
||||
const result = await patch("/settings", dataToSend as SettingsFormData);
|
||||
|
||||
if (result?.success) {
|
||||
// Update API key state from response
|
||||
|
||||
@@ -7,7 +7,7 @@ export const settingsSchema = z.object({
|
||||
systemEmailRejectUnauthorized: z.boolean(),
|
||||
systemEmailConnectionHost: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
@@ -19,31 +19,30 @@ export const settingsSchema = z.object({
|
||||
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
|
||||
pagespeedApiKey: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
systemEmailHost: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
systemEmailPort: z.number().int().min(1).max(65535).optional(),
|
||||
systemEmailAddress: z
|
||||
.string()
|
||||
.email("Please enter a valid email address")
|
||||
.or(z.literal(""))
|
||||
.transform((val) => (val === "" ? undefined : val.toLowerCase().trim()))
|
||||
.transform((val) => (val === "" ? null : val.toLowerCase().trim()))
|
||||
.optional(),
|
||||
systemEmailUser: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
systemEmailPassword: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
systemEmailTLSServername: z
|
||||
.string()
|
||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||
.optional(),
|
||||
globalThresholds: z.object({
|
||||
cpu: z.number().int().min(1).max(100),
|
||||
@@ -53,4 +52,5 @@ export const settingsSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export type SettingsFormInput = z.input<typeof settingsSchema>;
|
||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Settings } from "@/types/index.js";
|
||||
import type { Settings, SettingsUpdate } from "@/types/index.js";
|
||||
export interface ISettingsRepository {
|
||||
// create
|
||||
create(settings: Partial<Settings>): Promise<Settings>;
|
||||
// fetch
|
||||
findSingleton(): Promise<Settings | null>;
|
||||
// update
|
||||
update(settings: Partial<Settings>): Promise<Settings>;
|
||||
update(settings: SettingsUpdate): Promise<Settings>;
|
||||
// delete
|
||||
deleteLegacy: () => Promise<boolean>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose from "mongoose";
|
||||
import mongoose, { type UpdateQuery } from "mongoose";
|
||||
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
|
||||
import type { Settings } from "@/types/index.js";
|
||||
import type { Settings, SettingsUpdate } from "@/types/index.js";
|
||||
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
|
||||
|
||||
class MongoSettingsRepository implements ISettingsRepository {
|
||||
@@ -59,27 +59,23 @@ class MongoSettingsRepository implements ISettingsRepository {
|
||||
return this.toEntity(settings);
|
||||
};
|
||||
|
||||
update = async (settings: Partial<Settings>) => {
|
||||
const update: Record<string, any> = { $set: {}, $unset: {} };
|
||||
update = async (settings: SettingsUpdate) => {
|
||||
const $set: Record<string, unknown> = {};
|
||||
const $unset: Record<string, string> = {};
|
||||
|
||||
// Iterate through settings and separate into $set and $unset
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
if (value === undefined || value === "") {
|
||||
// Unset undefined or empty string values
|
||||
update.$unset[key] = "";
|
||||
if (value === undefined || value === null) {
|
||||
$unset[key] = "";
|
||||
} else {
|
||||
// Set defined values
|
||||
update.$set[key] = value;
|
||||
$set[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up empty operators
|
||||
if (Object.keys(update.$set).length === 0) {
|
||||
delete update.$set;
|
||||
}
|
||||
if (Object.keys(update.$unset).length === 0) {
|
||||
delete update.$unset;
|
||||
}
|
||||
const update: UpdateQuery<AppSettingsDocument> = {
|
||||
...(Object.keys($set).length > 0 && { $set }),
|
||||
...(Object.keys($unset).length > 0 && { $unset }),
|
||||
};
|
||||
|
||||
await AppSettingsModel.findOneAndUpdate({}, update, {
|
||||
upsert: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ISettingsRepository } from "@/repositories/index.js";
|
||||
import { Settings } from "@/types/index.js";
|
||||
import { Settings, SettingsUpdate } from "@/types/index.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ValidatedEnv } from "@/validation/envValidation.js";
|
||||
import type { StringValue } from "ms";
|
||||
@@ -19,7 +19,7 @@ export interface ISettingsService {
|
||||
loadSettings(): EnvConfig;
|
||||
getSettings(): EnvConfig;
|
||||
getDBSettings(): Promise<Settings>;
|
||||
updateDbSettings(newSettings: Partial<Settings>): Promise<Settings>;
|
||||
updateDbSettings(newSettings: SettingsUpdate): Promise<Settings>;
|
||||
}
|
||||
|
||||
export class SettingsService implements ISettingsService {
|
||||
@@ -54,7 +54,7 @@ export class SettingsService implements ISettingsService {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
updateDbSettings = async (newSettings: Partial<Settings>) => {
|
||||
updateDbSettings = async (newSettings: SettingsUpdate) => {
|
||||
return await this.settingsRepository.update(newSettings);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ export interface SettingsThresholds {
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export type SettingsUpdate = {
|
||||
[K in keyof Settings]?: Settings[K] | null;
|
||||
};
|
||||
|
||||
export interface Settings {
|
||||
id: string;
|
||||
checkTTL: number;
|
||||
|
||||
@@ -8,16 +8,16 @@ import { z } from "zod";
|
||||
export const updateAppSettingsBodyValidation = z
|
||||
.object({
|
||||
checkTTL: z.number().int().min(1).max(CHECK_TTL_SENTINEL).optional(),
|
||||
systemEmailPort: z.number().optional(),
|
||||
pagespeedApiKey: z.string().optional(),
|
||||
systemEmailPort: z.number().nullable().optional(),
|
||||
pagespeedApiKey: z.string().nullable().optional(),
|
||||
language: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
systemEmailHost: z.string().optional(),
|
||||
systemEmailAddress: z.string().optional(),
|
||||
systemEmailPassword: z.string().optional(),
|
||||
systemEmailUser: z.string().optional(),
|
||||
systemEmailConnectionHost: z.string().optional(),
|
||||
systemEmailTLSServername: z.string().optional(),
|
||||
systemEmailHost: z.string().nullable().optional(),
|
||||
systemEmailAddress: z.string().nullable().optional(),
|
||||
systemEmailPassword: z.string().nullable().optional(),
|
||||
systemEmailUser: z.string().nullable().optional(),
|
||||
systemEmailConnectionHost: z.string().nullable().optional(),
|
||||
systemEmailTLSServername: z.string().nullable().optional(),
|
||||
|
||||
showURL: z.boolean().optional(),
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
|
||||
Reference in new issue
Block a user