feat(forms): associate submissions with members by email match

submitForm now links member_id whenever submitter_email matches a space
member (including raw anonymous public submissions) -- owner-chosen
product decision, with the attribution tradeoff documented inline so a
future audit does not revert it. New pure escapeLike (ILIKE-safe email
match; '_' in addresses is literal) + pickMemberForEmail (deterministic
earliest-joined on duplicates) + 5 tests (suite 464). Shared
linkSubmissionsByEmail helper for reuse; updated the now-superseded
verified-only comment.
This commit is contained in:
Moheeb Zara
2026-05-16 23:44:03 -07:00
parent a364d152ca
commit ea521de342
3 changed files with 98 additions and 4 deletions
+21
View File
@@ -91,3 +91,24 @@ export function deriveFieldKeys<T extends { label: string }>(
return { ...f, key }
})
}
// Escape a literal so it can be used as a PostgREST/SQL ILIKE *value* and
// match exactly (case-insensitively) rather than as a pattern. Email local
// parts legitimately contain `_`, which is a single-char ILIKE wildcard --
// without escaping, `[email protected]` would also match `[email protected]`. Order matters:
// escape the escape char first.
export function escapeLike(s: string): string {
return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_')
}
// When an email matches more than one member row in a space (e.g. a manually
// added duplicate), associate deterministically with the earliest-joined one
// so the result is stable and reproducible.
export function pickMemberForEmail(
rows: Array<{ id: string; joined_at: string | null }>,
): string | null {
if (rows.length === 0) return null
return [...rows].sort((a, b) =>
(a.joined_at ?? '').localeCompare(b.joined_at ?? ''),
)[0].id
}