Contacts
Shared contacts pool — people linked polymorphically to customers, vendors, locations, etc. Each contact can carry multiple methods (email / phone) and can be linked to multiple entities.
Tables owned
| Table | Purpose |
|---|---|
org.contacts | Contact records (first/last name, title, notes). |
org.contact_methods | Email / phone methods attached to a contact, with label. |
org.entity_contacts | Polymorphic linker. (entity_type, entity_id, contact_id) with role + notes. entity_id is application-enforced (no DB FK). |
contact_methodsis owned here too — it's a subordinate ofcontactswith no independent consumers. Implicit in the table list above.
Operations
Writes — Contacts
createContact(conn, input)→Contact.input:{ firstName, lastName?, title?, notes? }.updateContact(conn, contactId, patch)→Contact. Partial update.deleteContact(conn, contactId)→void. Cascades to methods and entity_contact links.
Writes — Contact methods
addMethod(conn, contactId, { type, value, label? })→ContactMethod.type:'email' | 'phone'.updateMethod(conn, methodId, patch)→ContactMethod.deleteMethod(conn, methodId)→void.
Writes — Entity links
linkToEntity(conn, { contactId, entityType, entityId, role?, notes? })→EntityContact.entityTypeis theentity_typeenum.updateLink(conn, linkId, patch)→EntityContact. Edits role / notes.unlink(conn, linkId)→void. Removes the association without deleting the contact.
Reads — Contacts
listContacts(conn, filters?)→Contact[]. Filters:{ q? }(name/email search).getContactById(conn, contactId)→Contact | null.getContactsByIds(conn, contactIds)→Contact[]. Batch hydration.
Reads — Methods
listMethodsForContact(conn, contactId)→ContactMethod[].
Reads — Entity links
listContactsByEntity(conn, entityType, entityId)→Array<{ link: EntityContact, contact: Contact, methods: ContactMethod[] }>. The hydrated view used on Customer/Vendor detail pages.listEntityContactsByEntity(conn, entityType, entityId)→EntityContact[]. Link rows only (for L3 to compose its own hydration).getLinkById(conn, linkId)→EntityContact | null.
Notes
- The component never resolves
entity_idback to its source table. That's L3's job (it knows which component owns the entity). entity_typevalues are enum-controlled. Adding a new entity type requires a migration on the enum, not a code change here.