feat: validate PDF options with TDD tests
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 9m38s
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 9m38s
This commit is contained in:
parent
0e03e39ec7
commit
f89a3181f7
4 changed files with 335 additions and 0 deletions
88
src/utils/pdf-options.ts
Normal file
88
src/utils/pdf-options.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
const VALID_FORMATS = ["Letter", "Legal", "Tabloid", "Ledger", "A0", "A1", "A2", "A3", "A4", "A5", "A6"];
|
||||
const FORMAT_MAP = new Map(VALID_FORMATS.map(f => [f.toLowerCase(), f]));
|
||||
const PAGE_RANGES_RE = /^\d+(-\d*)?(\s*,\s*\d+(-\d*)?)*$/;
|
||||
const MARGIN_KEYS = new Set(["top", "right", "bottom", "left"]);
|
||||
|
||||
type PdfInput = Record<string, any>;
|
||||
type ValidResult = { valid: true; sanitized: Record<string, any> };
|
||||
type InvalidResult = { valid: false; error: string };
|
||||
|
||||
export function validatePdfOptions(opts: PdfInput): ValidResult | InvalidResult {
|
||||
if (!opts || typeof opts !== "object") return { valid: true, sanitized: {} };
|
||||
|
||||
const sanitized: Record<string, any> = {};
|
||||
|
||||
// scale
|
||||
if (opts.scale !== undefined) {
|
||||
if (typeof opts.scale !== "number" || opts.scale < 0.1 || opts.scale > 2.0) {
|
||||
return { valid: false, error: "scale must be a number between 0.1 and 2.0" };
|
||||
}
|
||||
sanitized.scale = opts.scale;
|
||||
}
|
||||
|
||||
// format
|
||||
if (opts.format !== undefined) {
|
||||
if (typeof opts.format !== "string") {
|
||||
return { valid: false, error: "format must be a string" };
|
||||
}
|
||||
const canonical = FORMAT_MAP.get(opts.format.toLowerCase());
|
||||
if (!canonical) {
|
||||
return { valid: false, error: `format must be one of: ${VALID_FORMATS.join(", ")}` };
|
||||
}
|
||||
sanitized.format = canonical;
|
||||
}
|
||||
|
||||
// booleans
|
||||
for (const field of ["landscape", "printBackground", "displayHeaderFooter", "preferCSSPageSize"]) {
|
||||
if (opts[field] !== undefined) {
|
||||
if (typeof opts[field] !== "boolean") {
|
||||
return { valid: false, error: `${field} must be a boolean` };
|
||||
}
|
||||
sanitized[field] = opts[field];
|
||||
}
|
||||
}
|
||||
|
||||
// width/height
|
||||
for (const field of ["width", "height"]) {
|
||||
if (opts[field] !== undefined) {
|
||||
if (typeof opts[field] !== "string") {
|
||||
return { valid: false, error: `${field} must be a string (CSS dimension)` };
|
||||
}
|
||||
sanitized[field] = opts[field];
|
||||
}
|
||||
}
|
||||
|
||||
// margin
|
||||
if (opts.margin !== undefined) {
|
||||
if (typeof opts.margin !== "object" || opts.margin === null || Array.isArray(opts.margin)) {
|
||||
return { valid: false, error: "margin must be an object with top/right/bottom/left string fields" };
|
||||
}
|
||||
for (const key of Object.keys(opts.margin)) {
|
||||
if (!MARGIN_KEYS.has(key)) {
|
||||
return { valid: false, error: `margin contains unknown key: ${key}` };
|
||||
}
|
||||
if (typeof opts.margin[key] !== "string") {
|
||||
return { valid: false, error: `margin.${key} must be a string` };
|
||||
}
|
||||
}
|
||||
sanitized.margin = { ...opts.margin };
|
||||
}
|
||||
|
||||
// pageRanges
|
||||
if (opts.pageRanges !== undefined) {
|
||||
if (typeof opts.pageRanges !== "string") {
|
||||
return { valid: false, error: "pageRanges must be a string" };
|
||||
}
|
||||
if (!PAGE_RANGES_RE.test(opts.pageRanges.trim())) {
|
||||
return { valid: false, error: "pageRanges must match pattern like '1-5', '1,3,5', or '2-'" };
|
||||
}
|
||||
sanitized.pageRanges = opts.pageRanges;
|
||||
}
|
||||
|
||||
// Pass through non-validated fields
|
||||
for (const key of ["headerTemplate", "footerTemplate"]) {
|
||||
if (opts[key] !== undefined) sanitized[key] = opts[key];
|
||||
}
|
||||
|
||||
return { valid: true, sanitized };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue