chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { FieldType, type BlueprintFieldDefinition } from "@planarchy/shared";
|
||||
|
||||
export interface CustomFieldValidationError {
|
||||
key: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a `dynamicFields` record against an array of BlueprintFieldDefinitions.
|
||||
* Returns an array of errors (empty = valid).
|
||||
*/
|
||||
export function validateCustomFields(
|
||||
fieldDefs: BlueprintFieldDefinition[],
|
||||
dynamicFields: Record<string, unknown>,
|
||||
): CustomFieldValidationError[] {
|
||||
const errors: CustomFieldValidationError[] = [];
|
||||
|
||||
for (const def of fieldDefs) {
|
||||
const value = dynamicFields[def.key];
|
||||
const isEmpty = value === undefined || value === null || value === "";
|
||||
|
||||
if (def.required && isEmpty) {
|
||||
errors.push({ key: def.key, message: `${def.label} is required` });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEmpty) continue;
|
||||
|
||||
switch (def.type) {
|
||||
case FieldType.NUMBER: {
|
||||
if (typeof value !== "number" && isNaN(Number(value))) {
|
||||
errors.push({ key: def.key, message: `${def.label} must be a number` });
|
||||
}
|
||||
const validation = def.validation;
|
||||
if (validation) {
|
||||
const num = Number(value);
|
||||
if (validation.min !== undefined && num < validation.min) {
|
||||
errors.push({ key: def.key, message: `${def.label} must be at least ${validation.min}` });
|
||||
}
|
||||
if (validation.max !== undefined && num > validation.max) {
|
||||
errors.push({ key: def.key, message: `${def.label} must be at most ${validation.max}` });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case FieldType.BOOLEAN:
|
||||
if (value !== true && value !== false && value !== "true" && value !== "false") {
|
||||
errors.push({ key: def.key, message: `${def.label} must be a boolean` });
|
||||
}
|
||||
break;
|
||||
|
||||
case FieldType.SELECT:
|
||||
if (def.options && def.options.length > 0) {
|
||||
const valid = def.options.some((o) => o.value === value);
|
||||
if (!valid) {
|
||||
const allowed = def.options.map((o) => o.label || o.value).join(", ");
|
||||
errors.push({ key: def.key, message: `${def.label} must be one of: ${allowed}` });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case FieldType.MULTI_SELECT:
|
||||
if (Array.isArray(value) && def.options && def.options.length > 0) {
|
||||
const validSet = new Set(def.options.map((o) => o.value));
|
||||
const invalid = (value as string[]).filter((v) => !validSet.has(v));
|
||||
if (invalid.length > 0) {
|
||||
errors.push({ key: def.key, message: `${def.label} contains invalid values: ${invalid.join(", ")}` });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case FieldType.URL:
|
||||
try {
|
||||
new URL(String(value));
|
||||
} catch {
|
||||
errors.push({ key: def.key, message: `${def.label} must be a valid URL` });
|
||||
}
|
||||
break;
|
||||
|
||||
case FieldType.EMAIL:
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value))) {
|
||||
errors.push({ key: def.key, message: `${def.label} must be a valid email address` });
|
||||
}
|
||||
break;
|
||||
|
||||
// TEXT, TEXTAREA, DATE — no structural validation beyond required
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
Reference in New Issue
Block a user