diff --git a/.changeset/strong-candles-yell.md b/.changeset/strong-candles-yell.md new file mode 100644 index 00000000..f17ea354 --- /dev/null +++ b/.changeset/strong-candles-yell.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Dedup keys were taking multiple tags, that was not according to NIP-01 behaviour. diff --git a/migrations/20260419_140000_normalize_parameterized_deduplication.js b/migrations/20260419_140000_normalize_parameterized_deduplication.js new file mode 100644 index 00000000..5be39f5c --- /dev/null +++ b/migrations/20260419_140000_normalize_parameterized_deduplication.js @@ -0,0 +1,31 @@ +exports.up = async function (knex) { + await knex.raw(` + WITH ranked AS ( + SELECT + id, + row_number() OVER ( + PARTITION BY event_pubkey, event_kind, jsonb_build_array(COALESCE(event_deduplication->>0, '')) + ORDER BY event_created_at DESC, event_id ASC + ) AS row_rank + FROM events + WHERE event_kind >= 30000 + AND event_kind < 40000 + ) + DELETE FROM events AS e + USING ranked AS r + WHERE e.id = r.id + AND r.row_rank > 1; + `) + + await knex.raw(` + UPDATE events + SET event_deduplication = jsonb_build_array(COALESCE(event_deduplication->>0, '')) + WHERE event_kind >= 30000 + AND event_kind < 40000 + AND event_deduplication IS DISTINCT FROM jsonb_build_array(COALESCE(event_deduplication->>0, '')); + `) +} + +exports.down = async function () { + // Irreversible data migration. +} diff --git a/src/services/event-import-service.ts b/src/services/event-import-service.ts index 25e6df1c..d02d9e59 100644 --- a/src/services/event-import-service.ts +++ b/src/services/event-import-service.ts @@ -26,11 +26,11 @@ const enrichEventMetadata = (event: Event): Event => { } if (isParameterizedReplaceableEvent(event)) { - const [, ...deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [ + const [, deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [ null, '', ] - enriched = { ...enriched, [EventDeduplicationMetadataKey]: deduplication } + enriched = { ...enriched, [EventDeduplicationMetadataKey]: deduplication ? [deduplication] : [''] } } return enriched as Event diff --git a/test/unit/services/event-import-service.spec.ts b/test/unit/services/event-import-service.spec.ts index 2f67eded..3c3cd590 100644 --- a/test/unit/services/event-import-service.spec.ts +++ b/test/unit/services/event-import-service.spec.ts @@ -3,7 +3,13 @@ import { join } from 'path' import fs from 'fs' import os from 'os' -import { EventImportLineError, EventImportService, EventImportStats } from '../../../src/services/event-import-service' +import { + createEventBatchPersister, + EventImportLineError, + EventImportService, + EventImportStats, +} from '../../../src/services/event-import-service' +import { EventDeduplicationMetadataKey, EventKinds, EventTags } from '../../../src/constants/base' import { Event } from '../../../src/@types/event' import { expect } from 'chai' import { getEvents } from '../data/events' @@ -170,4 +176,36 @@ describe('EventImportService', () => { expect(lineErrors.length).to.equal(0) } }) + + it('normalizes parameterized replaceable deduplication to first d tag value', async () => { + const parameterizedEvent: Event = { + id: 'a'.repeat(64), + pubkey: 'b'.repeat(64), + created_at: 1, + kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST, + tags: [[EventTags.Deduplication, 'one', 'two']], + content: 'hello', + sig: 'c'.repeat(128), + } + + let upsertedEvents: Event[] = [] + + const eventRepository = { + create: async () => 0, + createMany: async () => 0, + upsert: async () => 0, + upsertMany: async (events: Event[]) => { + upsertedEvents = events + return events.length + }, + deleteByPubkeyAndIds: async () => 0, + } as any + + const persistBatch = createEventBatchPersister(eventRepository) + const inserted = await persistBatch([parameterizedEvent]) + + expect(inserted).to.equal(1) + expect(upsertedEvents).to.have.length(1) + expect((upsertedEvents[0] as any)[EventDeduplicationMetadataKey]).to.deep.equal(['one']) + }) })