feat(contacts): add multi-value fields support to PUT /contacts/:id

Extend PUT /contacts/:id route to support updating phones, emails, and
addresses arrays with replacement semantics. Uses atomic transactions
to DELETE all existing multi-values then INSERT new ones. Validates
all multi-value fields before updating. Response includes full contact
with multi-value fields via loadMultiValueFields() helper.

Backward compatible: multi-value fields only replaced when present in
request body. Scalar fields (name, category, etc.) continue to work
independently.

Tests added:
- Update with multi-value fields (replacement semantics verified)
- Validation error on invalid phone data (400)
- Backward compatibility: update without multi-values preserves them

All 109 tests pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-05-04 18:31:01 +02:00
parent 9e346dca5f
commit 0dc303b81a
3 changed files with 323 additions and 31 deletions
+104 -20
View File
@@ -299,7 +299,7 @@ router.post('/', (req, res) => {
/**
* PUT /api/v1/contacts/:id
* Kontakt bearbeiten.
* Body: alle Felder optional
* Body: alle Felder optional, phones/emails/addresses mit Replacement-Semantik
* Response: { data: Contact }
*/
router.put('/:id', (req, res) => {
@@ -318,27 +318,111 @@ router.put('/:id', (req, res) => {
const errors = collectErrors(checks);
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
db.get().prepare(`
UPDATE contacts
SET name = COALESCE(?, name),
category = COALESCE(?, category),
phone = ?,
email = ?,
address = ?,
notes = ?
WHERE id = ?
`).run(
req.body.name?.trim() ?? null,
req.body.category ?? null,
req.body.phone !== undefined ? (req.body.phone?.trim() || null) : contact.phone,
req.body.email !== undefined ? (req.body.email?.trim() || null) : contact.email,
req.body.address !== undefined ? (req.body.address?.trim() || null) : contact.address,
req.body.notes !== undefined ? (req.body.notes?.trim() || null) : contact.notes,
id
);
// Validate multi-value fields if provided
if (req.body.phones !== undefined) {
const phonesValidation = validatePhones(req.body.phones);
if (!phonesValidation.valid) {
return res.status(400).json({ error: phonesValidation.error, code: 400 });
}
}
if (req.body.emails !== undefined) {
const emailsValidation = validateEmails(req.body.emails);
if (!emailsValidation.valid) {
return res.status(400).json({ error: emailsValidation.error, code: 400 });
}
}
if (req.body.addresses !== undefined) {
const addressesValidation = validateAddresses(req.body.addresses);
if (!addressesValidation.valid) {
return res.status(400).json({ error: addressesValidation.error, code: 400 });
}
}
// Update contact and multi-value fields in a transaction
const transaction = db.get().transaction(() => {
// Update scalar fields
db.get().prepare(`
UPDATE contacts
SET name = COALESCE(?, name),
category = COALESCE(?, category),
phone = ?,
email = ?,
address = ?,
notes = ?
WHERE id = ?
`).run(
req.body.name?.trim() ?? null,
req.body.category ?? null,
req.body.phone !== undefined ? (req.body.phone?.trim() || null) : contact.phone,
req.body.email !== undefined ? (req.body.email?.trim() || null) : contact.email,
req.body.address !== undefined ? (req.body.address?.trim() || null) : contact.address,
req.body.notes !== undefined ? (req.body.notes?.trim() || null) : contact.notes,
id
);
// Replace phones (delete all, insert new)
if (req.body.phones !== undefined && Array.isArray(req.body.phones)) {
db.get().prepare('DELETE FROM contact_phones WHERE contact_id = ?').run(id);
const insertPhone = db.get().prepare(`
INSERT INTO contact_phones (contact_id, label, value, is_primary)
VALUES (?, ?, ?, ?)
`);
for (const phone of req.body.phones) {
insertPhone.run(id, phone.label, phone.value, phone.isPrimary ? 1 : 0);
}
}
// Replace emails (delete all, insert new)
if (req.body.emails !== undefined && Array.isArray(req.body.emails)) {
db.get().prepare('DELETE FROM contact_emails WHERE contact_id = ?').run(id);
const insertEmail = db.get().prepare(`
INSERT INTO contact_emails (contact_id, label, value, is_primary)
VALUES (?, ?, ?, ?)
`);
for (const email of req.body.emails) {
insertEmail.run(id, email.label, email.value, email.isPrimary ? 1 : 0);
}
}
// Replace addresses (delete all, insert new)
if (req.body.addresses !== undefined && Array.isArray(req.body.addresses)) {
db.get().prepare('DELETE FROM contact_addresses WHERE contact_id = ?').run(id);
const insertAddress = db.get().prepare(`
INSERT INTO contact_addresses (contact_id, label, street, city, state, postal_code, country, is_primary)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const address of req.body.addresses) {
insertAddress.run(
id,
address.label,
address.street || null,
address.city || null,
address.state || null,
address.postalCode || null,
address.country || null,
address.isPrimary ? 1 : 0
);
}
}
});
transaction();
// Query the updated contact with multi-value fields
const updated = db.get().prepare('SELECT * FROM contacts WHERE id = ?').get(id);
res.json({ data: updated });
const multiValueFields = loadMultiValueFields(id);
res.json({
data: {
...updated,
...multiValueFields
}
});
} catch (err) {
log.error('', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 });