This commit is contained in:
Owen
2026-06-23 12:05:47 -04:00
parent 51c357e6c7
commit ce3c2f7583
4 changed files with 151 additions and 74 deletions
@@ -29,26 +29,40 @@ const paramsSchema = z.strictObject({
orgId: z.string().nonempty() orgId: z.string().nonempty()
}); });
const bodySchema = z.strictObject({ const bodySchema = z
name: z.string().nonempty(), .strictObject({
siteId: z.number().int().positive(), name: z.string().nonempty(),
hcEnabled: z.boolean().default(false), siteId: z.number().int().positive(),
hcMode: z.string().default("http"), hcEnabled: z.boolean().default(false),
hcHostname: z.string().optional(), hcMode: z.string().default("http"),
hcPort: z.number().int().min(1).max(65535).optional(), hcHostname: z.string().optional(),
hcPath: z.string().optional(), hcPort: z.number().int().min(1).max(65535).optional(),
hcScheme: z.string().optional(), hcPath: z.string().optional(),
hcMethod: z.string().default("GET"), hcScheme: z.string().optional(),
hcInterval: z.number().int().positive().default(30), hcMethod: z.string().default("GET"),
hcUnhealthyInterval: z.number().int().positive().default(30), hcInterval: z.number().int().positive().default(30),
hcTimeout: z.number().int().positive().default(1), hcUnhealthyInterval: z.number().int().positive().default(30),
hcHeaders: z.string().optional().nullable(), hcTimeout: z.number().int().positive().default(1),
hcFollowRedirects: z.boolean().default(true), hcHeaders: z.string().optional().nullable(),
hcStatus: z.number().int().optional().nullable(), hcFollowRedirects: z.boolean().default(true),
hcTlsServerName: z.string().optional(), hcStatus: z.number().int().optional().nullable(),
hcHealthyThreshold: z.number().int().positive().default(1), hcTlsServerName: z.string().optional(),
hcUnhealthyThreshold: z.number().int().positive().default(1) hcHealthyThreshold: z.number().int().positive().default(1),
}); hcUnhealthyThreshold: z.number().int().positive().default(1)
})
.superRefine((data, ctx) => {
const hcHostnameMissing =
data.hcHostname === undefined ||
data.hcHostname.trim().length === 0;
if (data.hcEnabled === true && hcHostnameMissing) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["hcHostname"],
message: "hcHostname is required when hcEnabled is true"
});
}
});
export type CreateHealthCheckResponse = { export type CreateHealthCheckResponse = {
targetHealthCheckId: number; targetHealthCheckId: number;
@@ -57,7 +71,6 @@ const CreateHealthCheckResponseDataSchema = z.object({
targetHealthCheckId: z.number() targetHealthCheckId: z.number()
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/health-check", path: "/org/{orgId}/health-check",
@@ -78,7 +91,9 @@ registry.registerPath({
description: "Successful response", description: "Successful response",
content: { content: {
"application/json": { "application/json": {
schema: createApiResponseSchema(CreateHealthCheckResponseDataSchema) schema: createApiResponseSchema(
CreateHealthCheckResponseDataSchema
)
} }
} }
} }
@@ -105,7 +105,6 @@ const UpdateHealthCheckResponseDataSchema = z.object({
hcUnhealthyThreshold: z.number().nullable() hcUnhealthyThreshold: z.number().nullable()
}); });
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/org/{orgId}/health-check/{healthCheckId}", path: "/org/{orgId}/health-check/{healthCheckId}",
@@ -126,7 +125,9 @@ registry.registerPath({
description: "Successful response", description: "Successful response",
content: { content: {
"application/json": { "application/json": {
schema: createApiResponseSchema(UpdateHealthCheckResponseDataSchema) schema: createApiResponseSchema(
UpdateHealthCheckResponseDataSchema
)
} }
} }
} }
@@ -215,6 +216,32 @@ export async function updateHealthCheck(
) )
.limit(1); .limit(1);
if (!existingHealthCheck) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Standalone health check not found"
)
);
}
const nextHcEnabled = hcEnabled ?? existingHealthCheck.hcEnabled;
const nextHcHostname =
hcHostname !== undefined
? hcHostname
: existingHealthCheck.hcHostname;
const hcHostnameMissing =
!nextHcHostname || nextHcHostname.trim().length === 0;
if (nextHcEnabled && hcHostnameMissing) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"hcHostname is required when hcEnabled is true"
)
);
}
if (name !== undefined) updateData.name = name; if (name !== undefined) updateData.name = name;
if (siteId !== undefined) updateData.siteId = siteId; if (siteId !== undefined) updateData.siteId = siteId;
if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled; if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled;
+53 -35
View File
@@ -33,41 +33,59 @@ const createTargetParamsSchema = z.strictObject({
resourceId: z.coerce.number().int().positive() resourceId: z.coerce.number().int().positive()
}); });
const createTargetSchema = z.strictObject({ const createTargetSchema = z
siteId: z.int().positive(), .strictObject({
ip: z.string().refine(isTargetValid), siteId: z.int().positive(),
mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(), ip: z.string().refine(isTargetValid),
method: z.string().optional().nullable(), mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(),
port: z.int().min(1).max(65535), method: z.string().optional().nullable(),
enabled: z.boolean().default(true), port: z.int().min(1).max(65535),
hcEnabled: z.boolean().optional(), enabled: z.boolean().default(true),
hcPath: z.string().min(1).optional().nullable(), hcEnabled: z.boolean().optional(),
hcScheme: z.string().optional().nullable(), hcPath: z.string().min(1).optional().nullable(),
hcMode: z.string().optional().nullable(), hcScheme: z.string().optional().nullable(),
hcHostname: z.string().optional().nullable(), hcMode: z.string().optional().nullable(),
hcPort: z.int().positive().optional().nullable(), hcHostname: z.string().optional().nullable(),
hcInterval: z.int().positive().min(1).optional().nullable(), hcPort: z.int().positive().optional().nullable(),
hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(), hcInterval: z.int().positive().min(1).optional().nullable(),
hcTimeout: z.int().positive().min(1).optional().nullable(), hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(),
hcHeaders: z hcTimeout: z.int().positive().min(1).optional().nullable(),
.array(z.strictObject({ name: z.string(), value: z.string() })) hcHeaders: z
.nullable() .array(z.strictObject({ name: z.string(), value: z.string() }))
.optional(), .nullable()
hcFollowRedirects: z.boolean().optional().nullable(), .optional(),
hcMethod: z.string().min(1).optional().nullable(), hcFollowRedirects: z.boolean().optional().nullable(),
hcStatus: z.int().optional().nullable(), hcMethod: z.string().min(1).optional().nullable(),
hcTlsServerName: z.string().optional().nullable(), hcStatus: z.int().optional().nullable(),
hcHealthyThreshold: z.int().positive().min(1).optional().nullable(), hcTlsServerName: z.string().optional().nullable(),
hcUnhealthyThreshold: z.int().positive().min(1).optional().nullable(), hcHealthyThreshold: z.int().positive().min(1).optional().nullable(),
path: z.string().optional().nullable(), hcUnhealthyThreshold: z.int().positive().min(1).optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), path: z.string().optional().nullable(),
rewritePath: z.string().optional().nullable(), pathMatchType: z
rewritePathType: z .enum(["exact", "prefix", "regex"])
.enum(["exact", "prefix", "regex", "stripPrefix"]) .optional()
.optional() .nullable(),
.nullable(), rewritePath: z.string().optional().nullable(),
priority: z.int().min(1).max(1000).optional().nullable() rewritePathType: z
}); .enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable(),
priority: z.int().min(1).max(1000).optional().nullable()
})
.superRefine((data, ctx) => {
const hcHostnameMissing =
data.hcHostname === undefined ||
data.hcHostname === null ||
data.hcHostname.trim().length === 0;
if (data.hcEnabled === true && hcHostnameMissing) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["hcHostname"],
message: "hcHostname is required when hcEnabled is true"
});
}
});
export type CreateTargetResponse = Target & TargetHealthCheck; export type CreateTargetResponse = Target & TargetHealthCheck;
+32 -15
View File
@@ -188,6 +188,38 @@ export async function updateTarget(
); );
} }
const [existingHc] = await db
.select()
.from(targetHealthCheck)
.where(eq(targetHealthCheck.targetId, targetId))
.limit(1);
if (!existingHc) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Health check for target with ID ${targetId} not found`
)
);
}
const nextHcEnabled = parsedBody.data.hcEnabled ?? existingHc.hcEnabled;
const nextHcHostname =
parsedBody.data.hcHostname !== undefined
? parsedBody.data.hcHostname
: existingHc.hcHostname;
const hcHostnameMissing =
!nextHcHostname || nextHcHostname.trim().length === 0;
if (nextHcEnabled && hcHostnameMissing) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"hcHostname is required when hcEnabled is true"
)
);
}
const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null; const pathMatchTypeRemoved = parsedBody.data.pathMatchType === null;
const nextMode = const nextMode =
parsedBody.data.mode === null ? undefined : parsedBody.data.mode; parsedBody.data.mode === null ? undefined : parsedBody.data.mode;
@@ -218,21 +250,6 @@ export async function updateTarget(
.where(eq(targets.targetId, targetId)) .where(eq(targets.targetId, targetId))
.returning(); .returning();
const [existingHc] = await trx
.select()
.from(targetHealthCheck)
.where(eq(targetHealthCheck.targetId, targetId))
.limit(1);
if (!existingHc) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Health check for target with ID ${targetId} not found`
)
);
}
let hcHeaders = null; let hcHeaders = null;
if (parsedBody.data.hcHeaders) { if (parsedBody.data.hcHeaders) {
hcHeaders = JSON.stringify(parsedBody.data.hcHeaders); hcHeaders = JSON.stringify(parsedBody.data.hcHeaders);