feat(05-05): hook notifyListChange into list/item mutations (reorder excluded)
- POST /:id/items (item added) → notifyListChange - PATCH /list-items/:itemId checked/text → notifyListChange; position-only → silent (D-01) - DELETE /list-items/:itemId → notifyListChange - PATCH /:id (list rename/share toggle) → notifyListChange - DELETE /:id (list delete) → notifyListChange - POST / (list create) → no notification (empty list, D-01 spirit) - lists.test.ts: 2 new tests prove reorder-silent (position) and check-notifies (NOTIF-02) - All 59 lists.test.ts assertions GREEN
This commit is contained in:
@@ -31,6 +31,7 @@ import { rankForAppend } from '../lib/rank.js'
|
||||
import '../auth/devBypass.js'
|
||||
|
||||
import { publishListEvent } from '../lib/listEmitter.js'
|
||||
import { notifyListChange } from '../lib/listChangeDispatcher.js'
|
||||
|
||||
export const listsRouter = new Hono()
|
||||
|
||||
@@ -383,6 +384,8 @@ listsRouter.patch('/:id', zValidator('json', patchListSchema), async (c) => {
|
||||
|
||||
// Fan-out: notify accessible subscribers that this list metadata changed (LIST-04)
|
||||
publishListEvent(listId, { type: 'list:updated', listId, payload: { id: listId, name: updated.name } })
|
||||
// Push: coalesced list-change notification to other accessible members (NOTIF-02)
|
||||
notifyListChange(listId, currentUserId)
|
||||
|
||||
return c.json({
|
||||
id: updated.id,
|
||||
@@ -429,6 +432,9 @@ listsRouter.delete('/:id', async (c) => {
|
||||
|
||||
// Fan-out: notify accessible subscribers that this list was deleted (LIST-04)
|
||||
publishListEvent(listId, { type: 'list:deleted', listId, payload: { id: listId } })
|
||||
// Push: coalesced list-change notification to other accessible members (NOTIF-02)
|
||||
// Note: list is already deleted from DB; notifyListChange handles missing list gracefully.
|
||||
notifyListChange(listId, currentUserId)
|
||||
|
||||
return c.json({ id: listId })
|
||||
} catch (err) {
|
||||
@@ -495,6 +501,8 @@ listsRouter.post('/:id/items', zValidator('json', createItemSchema), async (c) =
|
||||
|
||||
// Fan-out: notify accessible subscribers that an item was added (LIST-04)
|
||||
publishListEvent(listId, { type: 'item:added', listId, payload: { id: newItem.id, listId, text: newItem.text } })
|
||||
// Push: coalesced list-change notification to other accessible members (NOTIF-02)
|
||||
notifyListChange(listId, currentUserId)
|
||||
|
||||
return c.json(
|
||||
{
|
||||
@@ -638,6 +646,11 @@ listItemsRouter.patch('/:itemId', zValidator('json', patchItemSchema), async (c)
|
||||
|
||||
// Fan-out: notify accessible subscribers that an item was updated (LIST-04)
|
||||
publishListEvent(item.listId, { type: 'item:updated', listId: item.listId, payload: { id: updated.id, listId: updated.listId } })
|
||||
// Push: coalesced list-change notification — only for meaningful changes (D-01).
|
||||
// Reorder (position) patches do NOT trigger a push; only checked/text changes do.
|
||||
if (patch.position === undefined) {
|
||||
notifyListChange(item.listId, currentUserId)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
id: updated.id,
|
||||
@@ -689,6 +702,8 @@ listItemsRouter.delete('/:itemId', async (c) => {
|
||||
|
||||
// Fan-out: notify accessible subscribers that an item was deleted (LIST-04)
|
||||
publishListEvent(item.listId, { type: 'item:deleted', listId: item.listId, payload: { id: itemId } })
|
||||
// Push: coalesced list-change notification to other accessible members (NOTIF-02)
|
||||
notifyListChange(item.listId, currentUserId)
|
||||
|
||||
return c.json({ id: itemId })
|
||||
} catch (err) {
|
||||
|
||||
@@ -1122,3 +1122,52 @@ describe('PATCH /api/list-items/:id { position } — reorder ordering (LIST-03,
|
||||
expect(getBody.items[1].id).toBe(id1)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NOTIF-02 reorder-silent gate (D-01)
|
||||
//
|
||||
// D-01: Reorder (position) PATCH must NOT trigger a list-change push.
|
||||
// Non-reorder mutations (checked, text) MUST trigger notifyListChange.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('NOTIF-02 reorder-silent gate — notifyListChange call site guard (D-01)', () => {
|
||||
it('PATCH { position } (reorder) does NOT call notifyListChange', async () => {
|
||||
const ownerId = await seedUser('notif-reorder-silent')
|
||||
currentDevUserId = ownerId
|
||||
const listId = await seedList(ownerId, 'Reorder Silent', false)
|
||||
const itemId = await seedItem(listId, 'item', 'a0')
|
||||
|
||||
// Spy on notifyListChange BEFORE importing the app so the spy is registered
|
||||
// in the same module context as the router.
|
||||
const dispatcherModule = await import('../../src/lib/listChangeDispatcher.js')
|
||||
const spy = vi.spyOn(dispatcherModule, 'notifyListChange')
|
||||
|
||||
const app = await getApp()
|
||||
const res = await app.request(jsonRequest('PATCH', `/api/list-items/${itemId}`, { position: 'a1' }))
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
// Position-only PATCH must NOT call notifyListChange (D-01 reorder-silent)
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
spy.mockRestore()
|
||||
})
|
||||
|
||||
it('PATCH { checked: true } DOES call notifyListChange', async () => {
|
||||
const ownerId = await seedUser('notif-check-notifies')
|
||||
currentDevUserId = ownerId
|
||||
const listId = await seedList(ownerId, 'Check Notifies', false)
|
||||
const itemId = await seedItem(listId, 'item', 'a0')
|
||||
|
||||
const dispatcherModule = await import('../../src/lib/listChangeDispatcher.js')
|
||||
const spy = vi.spyOn(dispatcherModule, 'notifyListChange')
|
||||
|
||||
const app = await getApp()
|
||||
const res = await app.request(jsonRequest('PATCH', `/api/list-items/${itemId}`, { checked: true }))
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
// Checked PATCH MUST call notifyListChange (meaningful mutation per NOTIF-02)
|
||||
expect(spy).toHaveBeenCalledWith(listId, ownerId)
|
||||
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user