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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user