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
+197
View File
@@ -2445,4 +2445,201 @@ describe('Contacts API - Multi-Value Fields', () => {
assert.strictEqual(contact.addresses.length, 0);
});
});
describe('PUT /contacts/:id', () => {
it('should update contact with multi-value fields (replacement semantics)', async () => {
const contactsRouter = await import('./server/routes/contacts.js');
// First create a contact with multi-value fields
const createReq = {
params: {},
query: {},
body: {
name: 'Original Contact',
category: 'Arzt',
phones: [{ label: 'Mobil', value: '+49171111111', isPrimary: true }],
emails: [{ label: 'Privat', value: 'original@example.com', isPrimary: true }],
addresses: [{ label: 'Privat', street: 'Alte Straße 1', city: 'Berlin', postalCode: '10115', country: 'Deutschland', isPrimary: true }]
}
};
const createRes = {
statusCode: 200,
status(code) { this.statusCode = code; return this; },
json(data) { this.data = data; return this; },
};
const postHandler = contactsRouter.default.stack.find(
layer => layer.route?.path === '/' && layer.route.methods.post
)?.route?.stack[0]?.handle;
await postHandler(createReq, createRes);
const contactId = createRes.data.data.id;
// Now update it with new multi-value fields (replacement)
const updateReq = {
params: { id: String(contactId) },
query: {},
body: {
name: 'Updated Contact',
phones: [
{ label: 'Arbeit', value: '+49302222222', isPrimary: true },
{ label: 'Mobil', value: '+49173333333', isPrimary: false }
],
emails: [
{ label: 'Arbeit', value: 'new.work@example.com', isPrimary: true }
],
addresses: [
{ label: 'Arbeit', street: 'Neue Straße 10', city: 'München', postalCode: '80331', country: 'Deutschland', isPrimary: true }
]
}
};
const updateRes = {
statusCode: 200,
status(code) { this.statusCode = code; return this; },
json(data) { this.data = data; return this; },
};
const putHandler = contactsRouter.default.stack.find(
layer => layer.route?.path === '/:id' && layer.route.methods.put
)?.route?.stack[0]?.handle;
await putHandler(updateReq, updateRes);
assert.strictEqual(updateRes.statusCode, 200);
const updated = updateRes.data.data;
// Check name was updated
assert.strictEqual(updated.name, 'Updated Contact');
// Check phones were replaced (old deleted, new inserted)
assert.strictEqual(updated.phones.length, 2);
assert.strictEqual(updated.phones[0].label, 'Arbeit');
assert.strictEqual(updated.phones[0].value, '+49302222222');
assert.strictEqual(updated.phones[0].isPrimary, true);
assert.strictEqual(updated.phones[1].label, 'Mobil');
assert.strictEqual(updated.phones[1].value, '+49173333333');
assert.strictEqual(updated.phones[1].isPrimary, false);
// Check emails were replaced
assert.strictEqual(updated.emails.length, 1);
assert.strictEqual(updated.emails[0].label, 'Arbeit');
assert.strictEqual(updated.emails[0].value, 'new.work@example.com');
assert.strictEqual(updated.emails[0].isPrimary, true);
// Check addresses were replaced
assert.strictEqual(updated.addresses.length, 1);
assert.strictEqual(updated.addresses[0].label, 'Arbeit');
assert.strictEqual(updated.addresses[0].street, 'Neue Straße 10');
assert.strictEqual(updated.addresses[0].city, 'München');
assert.strictEqual(updated.addresses[0].isPrimary, true);
});
it('should return 400 for invalid multi-value data', async () => {
const contactsRouter = await import('./server/routes/contacts.js');
// Create a contact first
const createReq = {
params: {},
query: {},
body: {
name: 'Test Contact',
category: 'Sonstiges'
}
};
const createRes = {
statusCode: 200,
status(code) { this.statusCode = code; return this; },
json(data) { this.data = data; return this; },
};
const postHandler = contactsRouter.default.stack.find(
layer => layer.route?.path === '/' && layer.route.methods.post
)?.route?.stack[0]?.handle;
await postHandler(createReq, createRes);
const contactId = createRes.data.data.id;
// Try to update with invalid phone data
const updateReq = {
params: { id: String(contactId) },
query: {},
body: {
phones: [{ label: 'Mobil', value: '', isPrimary: 'invalid' }] // empty value + invalid isPrimary
}
};
const updateRes = {
statusCode: 200,
status(code) { this.statusCode = code; return this; },
json(data) { this.data = data; return this; },
};
const putHandler = contactsRouter.default.stack.find(
layer => layer.route?.path === '/:id' && layer.route.methods.put
)?.route?.stack[0]?.handle;
await putHandler(updateReq, updateRes);
assert.strictEqual(updateRes.statusCode, 400);
assert.ok(updateRes.data.error, 'Should have error message');
});
it('should update contact without multi-value fields (backwards compatible)', async () => {
const contactsRouter = await import('./server/routes/contacts.js');
// Create contact with multi-value fields
const createReq = {
params: {},
query: {},
body: {
name: 'Test Contact',
category: 'Sonstiges',
phones: [{ label: 'Mobil', value: '+49171111111', isPrimary: true }]
}
};
const createRes = {
statusCode: 200,
status(code) { this.statusCode = code; return this; },
json(data) { this.data = data; return this; },
};
const postHandler = contactsRouter.default.stack.find(
layer => layer.route?.path === '/' && layer.route.methods.post
)?.route?.stack[0]?.handle;
await postHandler(createReq, createRes);
const contactId = createRes.data.data.id;
// Update without touching multi-value fields (only scalar fields)
const updateReq = {
params: { id: String(contactId) },
query: {},
body: {
name: 'Updated Name Only',
category: 'Arzt'
}
};
const updateRes = {
statusCode: 200,
status(code) { this.statusCode = code; return this; },
json(data) { this.data = data; return this; },
};
const putHandler = contactsRouter.default.stack.find(
layer => layer.route?.path === '/:id' && layer.route.methods.put
)?.route?.stack[0]?.handle;
await putHandler(updateReq, updateRes);
assert.strictEqual(updateRes.statusCode, 200);
const updated = updateRes.data.data;
// Scalar fields should be updated
assert.strictEqual(updated.name, 'Updated Name Only');
assert.strictEqual(updated.category, 'Arzt');
// Multi-value fields should remain unchanged
assert.strictEqual(updated.phones.length, 1);
assert.strictEqual(updated.phones[0].value, '+49171111111');
});
});
});