fix(audit): import cluster — M27 (commit idempotency), M25 (in-file dedup preview), M26 (undo destructive-update reporting), L33 (mapping/mooring), L35 (port-auth doc)
M25 DB unique-index backstop deferred: needs a migration (column + backfill + insert-stamp trigger + dedup) — tracked as a follow-up. The classify in-file dedup (preview accuracy) ships now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,28 @@ import { createBerth, updateBerth } from '@/lib/services/berths.service';
|
||||
|
||||
import type { ImportAdapter, MappedRow } from '../types';
|
||||
|
||||
/**
|
||||
* Accepted import spellings of a mooring: letters, an optional separating
|
||||
* hyphen, optional leading zeros, then 1–6 digits. The 6-digit cap (audit
|
||||
* L33(b)) rejects absurd numbers that would overflow JS's safe-integer range
|
||||
* during canonicalization — a real marina mooring is at most a few thousand.
|
||||
* Canonicalization strips the hyphen + leading zeros and upper-cases the
|
||||
* letters, so the *output* always conforms to the canonical `^[A-Z]+\d+$`.
|
||||
*/
|
||||
const MOORING_INPUT_RE = /^[A-Za-z]+-?0*\d{1,6}$/;
|
||||
/** Canonical stored form — the post-canonicalization invariant. */
|
||||
const MOORING_CANON_RE = /^[A-Z]+\d+$/;
|
||||
|
||||
/** Canonicalize a mooring to the unified `^[A-Z]+\d+$` form ("A1", "D32"):
|
||||
* uppercase letters, drop a hyphen + leading zeros on the number. */
|
||||
* uppercase letters, drop a hyphen + leading zeros on the number. The number
|
||||
* is normalized digit-wise (no parseInt) so values up to the 6-digit input
|
||||
* cap survive without floating-point/MAX_SAFE_INTEGER precision loss. */
|
||||
function canonMoo(raw: string): string {
|
||||
const m = /^([A-Za-z]+)-?0*(\d+)$/.exec(raw.trim());
|
||||
return m ? `${m[1]!.toUpperCase()}${parseInt(m[2]!, 10)}` : raw.trim().toUpperCase();
|
||||
if (!m) return raw.trim().toUpperCase();
|
||||
// Drop leading zeros without parseInt; keep a lone "0" as "0".
|
||||
const digits = m[2]!.replace(/^0+(?=\d)/, '');
|
||||
return `${m[1]!.toUpperCase()}${digits}`;
|
||||
}
|
||||
|
||||
const num = (s: string | undefined): number | undefined =>
|
||||
@@ -28,7 +45,12 @@ export const berthsAdapter: ImportAdapter = {
|
||||
label: 'Mooring number',
|
||||
required: true,
|
||||
aliases: ['mooring', 'berth', 'berthnumber'],
|
||||
zod: z.string().regex(/^[A-Za-z]+-?0*\d+$/, 'Use a form like A1, B12, E18'),
|
||||
zod: z
|
||||
.string()
|
||||
.regex(MOORING_INPUT_RE, 'Use a form like A1, B12, E18 (max 6 digits)')
|
||||
// Defense in depth: whatever the input spelling, the canonical form
|
||||
// must conform to ^[A-Z]+\d+$ (audit L33(b)).
|
||||
.refine((v) => MOORING_CANON_RE.test(canonMoo(v)), 'Invalid mooring format'),
|
||||
},
|
||||
{
|
||||
key: 'area',
|
||||
|
||||
Reference in New Issue
Block a user