syntaxai/tdd.md · main · src / a31_edit_validation.ts

a31_edit_validation.ts 39 lines · 1436 bytes raw
// c31 — model: validation for an admin edit submission. Pure: no I/O.
// The DB no longer stores edits (admin POST goes directly to Forgejo
// + filesystem), so this file holds only the body sanity checks that
// were previously bundled with the SQLite proposal flow.

export const MAX_EDIT_BODY_BYTES = 256 * 1024; // 256 KB

export class EditValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "EditValidationError";
  }
}

// Throws EditValidationError when the body is empty, too large, or
// otherwise unfit to commit. Returns the trimmed-but-otherwise-untouched
// body string on success.
export const validateEditBody = (raw: unknown): string => {
  if (typeof raw !== "string") {
    throw new EditValidationError("body must be a string");
  }
  if (raw.trim().length === 0) {
    throw new EditValidationError("body cannot be empty");
  }
  const bytes = new TextEncoder().encode(raw).length;
  if (bytes > MAX_EDIT_BODY_BYTES) {
    throw new EditValidationError(
      `body exceeds the ${MAX_EDIT_BODY_BYTES / 1024} KB limit (got ${Math.round(bytes / 1024)} KB)`,
    );
  }
  return raw;
};

// Byte-identical check between current page content and the proposed
// new content. Used to skip a Forgejo round-trip when the user
// accidentally submitted without changes.
export const isNoOpEdit = (currentBody: string, newBody: string): boolean =>
  currentBody === newBody;