Commit 5f80917
authored
fix(migration): delete orphan extension rows on phone-only migration (#2842)
* feat: create oauth client utilities for mining-sources edge function
* fix: address code review issues - pin deps, add guards, fix types
* feat: create mining-sources edge function with OAuth routes
* fix: address code review issues - add onError handler, fix credentials JSON, add getRequiredEnv
* test: add tests for mining-sources utility functions and route schemas
* test: extract schemas to shared module, strengthen test assertions, add edge cases
* feat: update frontend to call mining-sources edge function
* chore: remove deprecated Express OAuth routes and methods
* chore: remove deprecated add-mining-source edge function (replaced by mining-sources)
* test: remove deprecated OAuth helper tests (functionality moved to edge function)
* fix: resolve CI lint and DeepSource issues
* feat: make oauth_callback_base_url a required env var with no fallbacks
* docs: mark oauth_callback_base_url as required in env template
* feat: add mining-sources config and document oauth_callback_base_url env var
* fix: hmac-sign oauth state to prevent userId forgery
* feat: add callbackQuerySchema for oauth callback route validation
* test: add callbackQuerySchema tests
* feat: use zod validation for oauth callback route params
* chore: remove orphaned validator schemas and stale add-mining-source directory
* fix: remove dead OAuth code from mining controller (post-merge cleanup)
* feat: add create_smtp_sender_for_oauth rpc for smtp twin creation
* feat: add smtp twin creation to mining-sources edge function callback
* test: add smtp twin creation tests for mining-sources edge function
* chore: remove plan docs from remote
* chore: remove workmux config from remote
* fix: use const instead of let for accessToken/refreshToken (deepsource)
* feat(db): drop person_email, key extension tables by persons.id
* chore(types): regenerate supabase types after person_id migration
* feat: support phone-only contacts across backend stack
Update all backend code to use person UUIDs (persons.id) instead of
email as the primary identifier, matching the DB migration that drops
the NOT NULL constraint on persons.email.
- PgContacts: SQL constants and method bodies now use person_id;
addManyAndReturn returns id+email+tags; new getPersonIdByEmail helper
- Contacts interface: renamed email params to ids, new helper method,
updateManyPersonsStatus uses {id, status} tuples
- types.ts: Person.email is optional with Person.id added; Contact and
ContactFrontend gain email? and identifier? fields
- contacts.controller: request body field 'emails' renamed to 'ids',
registerExportedContacts receives UUIDs
- contacts-verification.controller: looks up person_id via
getPersonIdByEmail before calling updateManyPersonsStatus
- enrichments: filters contacts by id+email, registers engagements
with person_id
- engagements.register: payload uses person_id instead of email
- Extractors (EmailMessage, FileImport, GoogleContactsExtractor,
PostgreSQLImport) and Google export updated to handle optional
email; phone-only contacts are skipped or guarded where appropriate
- Tests updated: enrichments fixtures include id; pg-contacts mock
rows include {id, email}
* feat(frontend): support phone-only contacts across the UI
Update the Vue stack to identify contacts by person UUID instead of
email, matching the DB migration that drops the NOT NULL constraint
on persons.email.
Frontend Contact type:
- email is now optional; new getContactIdentifier() helper returns
email or first phone for display
- ContactEdit.email also optional
MiningTable.vue:
- data-key switched from 'email' to 'id'
- contactsToTreat now maps to id (used by ExportContacts and
RemoveContactButton)
- 'Send by Email' button disables when no selected contact has email
- Refresh-status pill guards against contacts without an email
- Identifier (email or phone) shown in the contacts column
- selectedEmails store key renamed to selectedIds
ExportContacts.vue:
- Sends 'ids' instead of 'emails' in the export body
- contactsToTreat v-model is now ids
ContactInformationSidebar.vue:
- contacts-to-treat prop, real-time subscription, and delete
operations all key off person.id
- copy button hides for phone-only contacts
- refresh status no-ops without an email
Pinia contacts store:
- selectedEmails ref renamed to selectedIds
- Cache, updates, removes and realtime handler all key off contact.id
(was keyed off email)
- hasPersons probe selects 'id' instead of 'email'
utils/contacts.ts:
- updateContact / updateContactTags now key off person_id
- removeContactsFromDatabase passes 'ids' to delete_contacts RPC
- delete_contacts RPC signature changed from emails text[] to
ids uuid[] so phone-only contacts can be partially deleted
CampaignComposerDialog.vue:
- selectedEmails computed filters out phone-only contacts (email
campaign can't reach them)
Regenerated database.types.ts after the migration was applied
(strips CLI noise, 1509 lines).
* test(backend): cover phone-only contact round-trip in PgContacts
* test(backend): ensure updateManyPersonsStatus works for phone-only contacts
* feat(frontend): hide email-only widgets in sidebar for phone-only contacts
* fix(phone-only): address critical and major review findings
- C1: email-campaigns edge function rewrites getContactsByEmails
to resolve emails -> person_ids -> get_contacts_table_by_ids
(replaces call to dropped get_contacts_table_by_emails RPC)
- C2: frontend removeContactsFromDatabase now passes ids[]
to private.delete_contacts RPC (was passing emails[])
- M1: backend Google export mapToPerson skips null/empty email
instead of writing { value: undefined } into emailAddresses
- M2: delete_contacts message DELETE now scopes on m.user_id to
prevent cross-user message leak when message_ids are shared
- M3: delete_contacts parameter renamed user_id->p_user_id,
ids->p_ids, deleteallcontacts->p_delete_all for consistency
with other private.* functions and to avoid column shadowing
Verified:
- Local Supabase: 2-step email->id->contact lookup returns
email contacts and correctly excludes phone-only contacts
- delete_contacts partial delete and bulk delete both work
- Backend 588/588 tests pass, build clean
- Frontend 72/100 tests pass (28 pre-existing failures, same
as baseline of 34 on main)
- Frontend typecheck 14 errors (down from 29 baseline; all
remaining are pre-existing)
- Prettier clean
* fix(phone-only): address minor review cleanups
- m1: INSERT_TAGS_SQL now uses explicit ON CONFLICT
(person_id, name, user_id) target instead of relying on
Postgres inference (was 'ON CONFLICT DO NOTHING')
- g: remove-contact confirmation dialog falls back to
getContactIdentifier (phone) when name and email are empty
- h: MiningTable global search now includes 'identifier' and
'telephone' so phone-only contacts are findable
- i: EnrichButton filters out contacts with null email
instead of casting to string (phone-only contacts cannot
be enriched by email)
- j: frontend callers updated to p_user_id parameter name;
stale @ts-expect-error directives removed where the
regenerated database.types.ts now types RPCs correctly
(contacts store: get_contacts_table, refine_persons;
sources: get_mining_source_overview; campaigns:
get_campaigns_overview, get_sms_campaigns_overview;
contacts utils: organizations CRUD, persons update)
- regen: database.types.ts regenerated against updated
migration; CLI noise stripped (1986 lines)
* fix(frontend): replace non-null assertion in getContactIdentifier
DeepSource JS-0339: forbidden non-null assertion. Use ?? '' fallback
to match the function's existing return semantics.
* fix(frontend): rename unused destructure members to silence DeepSource
DeepSource JS-0356: 'tags' and 'sources' are assigned but never used
in the destructure block. Follow the existing _id/_userId convention.
* fix(extractor): add early-skip guard for person.email in EmailMessage
DeepSource JS-0339: 4x forbidden non-null assertions on person.email.
The extractContacts method is email-specific; in practice person.email
is always set. Add an early return guard to make the type system happy
and prevent any future crash if a phone-only contact somehow flows in.
* fix(extractor): skip phone-only contacts in FileImport
DeepSource JS-0339: forbidden non-null assertion on email in
FileImport.ts getContacts().
Phone-only contacts from CSV/XLSX imports have no email to verify.
The existing assert(Boolean(email)) in extractPerson was a runtime
non-null assertion in disguise; remove it so phone-only contacts
flow through extractPerson, and add an early return guard in
getContacts before the email.split('@') call.
Add a unit test that constructs a phone-only contact and verifies:
- getContacts() returns an empty persons array
- domainStatusVerification is not called
- the internal Promise.allSettled does not silently reject
(catches the underlying null-split crash that was previously
swallowed by Promise.allSettled)
* fix(extractor): skip phone-only contacts in PostgreSQLImport
DeepSource JS-0339: forbidden non-null assertion on email in
PostgreSQLImport.ts getContacts().
Phone-only contacts from a PostgreSQL mining source have no email
to verify. Add an early return guard before the email.split('@') call.
Add a unit test that constructs a phone-only contact and verifies:
- getContacts() returns an empty persons array
- domainStatusVerification is not called
- the internal Promise.allSettled does not silently reject
(catches the underlying null-split crash that was previously
swallowed by Promise.allSettled)
* style(backend): apply prettier to new extractor tests
CI prettier check failed on fileImport.test.ts and postgresqlImport.test.ts.
Auto-format applied.
* fix(backend): address 3 DeepSource issues in new test files
* chore(google-contacts-export): add skipcq comment on mapToPerson
DeepSource JS-R1005: mapToPerson has cyclomatic complexity 33.
Pre-existing on main; refactor tracked in #2831. Silenced with skipcq
to clean up the PR's DeepSource report. The function maps 7+ schema
field types (names, emails, phones, orgs, urls, addresses, memberships)
in one pass, and the per-field 'is duplicate' checks against existing
fields inherently inflate complexity. Splitting into helpers would be
a behavioral change and out of scope for PR #2830.
* fix(extractor): process phone-only contacts in FileImport and PostgreSQLImport
The earlier DeepSource cleanup added 'if (!email) return;' guards to skip
phone-only contacts in both extractors. This was wrong: these extractors
can receive phone-only contacts from CSV/PostgreSQL mining sources, and
the whole point of the phone-only support refactor is to surface them
in the contact list.
Behavior now matches GoogleContactsExtractor (which always adds the
person to persons and defaults to a 'personal' tag with REACHABILITY.NONE
when no email is available). Pre-existing on main in GoogleContactsExtractor;
this aligns FileImport and PostgreSQLImport with that pattern.
Changes:
- FileImport: phone-only contacts added with personal/REACHABILITY.DIRECT_PERSON/
refined#phone_only tag, bypassing domain verification and email-based tagging
- PostgreSQLImport: same, plus mapRowToContact no longer throws on missing
email (was filtering out phone-only rows at the mapping layer)
- TagSource union extended with 'refined#phone_only'
- Tests updated: fileImport now asserts phone-only IS processed; added a
second test verifying invalid-domain email contacts are still dropped
- postgresqlImport now asserts phone-only IS processed
EmailMessage engine is unchanged: it is email-specific by design, and
contacts from email mailboxes always have emails.
Tests: 592/592 pass (was 591; +1 new test case for invalid-domain drop).
* fix(migration): delete orphan extension rows before person_id backfill1 parent 7903e9b commit 5f80917
1 file changed
Lines changed: 109 additions & 0 deletions
Lines changed: 109 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
71 | 103 | | |
72 | 104 | | |
73 | 105 | | |
| |||
114 | 146 | | |
115 | 147 | | |
116 | 148 | | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
117 | 174 | | |
118 | 175 | | |
119 | 176 | | |
| |||
152 | 209 | | |
153 | 210 | | |
154 | 211 | | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
155 | 238 | | |
156 | 239 | | |
157 | 240 | | |
| |||
185 | 268 | | |
186 | 269 | | |
187 | 270 | | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
188 | 297 | | |
189 | 298 | | |
190 | 299 | | |
| |||
0 commit comments