From 1d8c124a7e64177bbfa608ca2e5310f64f2e747d Mon Sep 17 00:00:00 2001 From: Kanishka Date: Sat, 25 Apr 2026 20:45:26 +0530 Subject: [PATCH] fix: allow empty generic tag filter values --- .changeset/generic-tags-empty-values.md | 5 +++++ src/schemas/filter-schema.ts | 2 +- .../repositories/event-repository.spec.ts | 12 ++++++++++++ test/unit/schemas/filter-schema.spec.ts | 14 +++++++++++--- test/unit/utils/event.spec.ts | 19 +++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 .changeset/generic-tags-empty-values.md diff --git a/.changeset/generic-tags-empty-values.md b/.changeset/generic-tags-empty-values.md new file mode 100644 index 00000000..a9e68e09 --- /dev/null +++ b/.changeset/generic-tags-empty-values.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Allow generic tag filters to match empty string tag values. diff --git a/src/schemas/filter-schema.ts b/src/schemas/filter-schema.ts index e27817c4..1aa41897 100644 --- a/src/schemas/filter-schema.ts +++ b/src/schemas/filter-schema.ts @@ -14,7 +14,7 @@ export const filterSchema = z until: createdAtSchema.optional(), limit: z.number().int().min(0).optional(), }) - .catchall(z.array(z.string().min(1).max(1024))) + .catchall(z.array(z.string().max(1024))) .superRefine((data, ctx) => { for (const key of Object.keys(data)) { if (!knownFilterKeys.has(key) && !isGenericTagQuery(key)) { diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index 0ca0c0a4..6a9ffbfa 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -383,6 +383,18 @@ describe('EventRepository', () => { ) }) }) + + describe('#d', () => { + it('selects events by empty #d tag value', () => { + const filters = [{ '#d': [''] }] + + const query = repository.findByFilters(filters).toString() + + expect(query).to.equal( + 'select "events".* from "events" left join "event_tags" on "events"."event_id" = "event_tags"."event_id" where (event_tags.tag_name = \'d\' AND event_tags.tag_value = \'\') order by "event_created_at" asc, "event_id" asc limit 500', + ) + }) + }) }) describe('2 filters', () => { diff --git a/test/unit/schemas/filter-schema.spec.ts b/test/unit/schemas/filter-schema.spec.ts index 92683751..d6720008 100644 --- a/test/unit/schemas/filter-schema.spec.ts +++ b/test/unit/schemas/filter-schema.spec.ts @@ -55,6 +55,17 @@ describe('NIP-01', () => { expect(result.value).to.deep.equal(filter) }) + it('accepts empty strings in generic tag filters', () => { + const filterWithEmptyTagValue = { + '#d': [''], + } + + const result = validateSchema(filterSchema)(filterWithEmptyTagValue) + + expect(result.error).to.be.undefined + expect(result.value).to.deep.equal(filterWithEmptyTagValue) + }) + const cases = { ids: [{ message: 'must be an array', transform: assocPath(['ids'], null) }], prefixOrId: [ @@ -103,7 +114,6 @@ describe('NIP-01', () => { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)), }, - { message: 'is not allowed to be empty', transform: assocPath(['#e', 0], '') }, ], '#p': [{ message: 'must be an array', transform: assocPath(['#p'], null) }], '#p[0]': [ @@ -111,7 +121,6 @@ describe('NIP-01', () => { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)), }, - { message: 'is not allowed to be empty', transform: assocPath(['#p', 0], '') }, ], '#r': [{ message: 'must be an array', transform: assocPath(['#r'], null) }], '#r[0]': [ @@ -119,7 +128,6 @@ describe('NIP-01', () => { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)), }, - { message: 'is not allowed to be empty', transform: assocPath(['#r', 0], '') }, ], } diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index d9b2ccf3..f059f940 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -258,6 +258,25 @@ describe('NIP-01', () => { describe('NIP-12', () => { let event: Event + + describe('#d filter', () => { + beforeEach(() => { + event = { + id: 'cf8de9db67a1d7203512d1d81e6190f5e53abfdc0ac90275f67172b65a5b09a0', + pubkey: 'e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7', + created_at: 1645030752, + kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST, + tags: [['d', '']], + content: 'empty d tag', + sig: '53d12018d036092794366283eca36df4e0cabd014b6e91bbf684c8bb9bbbe9dedafa77b6b928587e11e05e036227598dded8713e8da17d55076e12242b361542', + } + }) + + it('returns true if #d filter contains an empty tag value in the event', () => { + expect(isEventMatchingFilter({ '#d': [''] })(event)).to.be.true + }) + }) + describe('#r filter', () => { beforeEach(() => { event = {