Skip to content

Commit a31e95a

Browse files
committed
feat: support geohash prefix matching for #g filters
1 parent ce59383 commit a31e95a

5 files changed

Lines changed: 91 additions & 3 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"nostream": patch
3+
---
4+
5+
Implement geohash wildcard/prefix behavior for `#g` filters (closes #265): a
6+
criterion ending in `*` matches any event `g` tag whose value starts with the
7+
prefix before `*`; exact matching (no `*`) is unchanged. Only normal geohash
8+
prefixes are intended as input. This is a Nostream extension, not part of
9+
NIP-12.

src/repositories/event-repository.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ const groupByLengthSpec = groupBy<string, 'exact' | 'even' | 'odd'>(
5858

5959
const logger = createLogger('event-repository')
6060

61+
const isGeohashPrefixCriterion = (filterName: string, criterion: string): boolean =>
62+
filterName === '#g' && criterion.endsWith('*')
63+
64+
const stripGeohashPrefixWildcard = (criterion: string): string => criterion.slice(0, -1)
65+
6166
export class EventRepository implements IEventRepository {
6267
public constructor(
6368
private readonly masterDbClient: DatabaseClient,
@@ -193,8 +198,21 @@ export class EventRepository implements IEventRepository {
193198
isEmpty,
194199
() => andWhereRaw('1 = 0', bd),
195200
forEach(
196-
(criterion: string) =>
197-
void orWhereRaw('event_tags.tag_name = ? AND event_tags.tag_value = ?', [filterName[1], criterion], bd),
201+
(criterion: string) => {
202+
if (isGeohashPrefixCriterion(filterName, criterion)) {
203+
return void orWhereRaw(
204+
'event_tags.tag_name = ? AND event_tags.tag_value LIKE ?',
205+
[filterName[1], `${stripGeohashPrefixWildcard(criterion)}%`],
206+
bd,
207+
)
208+
}
209+
210+
return void orWhereRaw(
211+
'event_tags.tag_name = ? AND event_tags.tag_value = ?',
212+
[filterName[1], criterion],
213+
bd,
214+
)
215+
},
198216
),
199217
)(criteria)
200218
})

src/utils/event.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ export const isEventMatchingFilter =
4040
(filter: SubscriptionFilter) =>
4141
(event: Event): boolean => {
4242
const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix)
43+
const isMatchingGenericTagCriterion = (key: string, criterion: string) => (tag: Tag): boolean => {
44+
const [, tagName] = key
45+
if (tag[0] !== tagName) {
46+
return false
47+
}
48+
49+
if (key === '#g' && criterion.endsWith('*')) {
50+
return tag[1].startsWith(criterion.slice(0, -1))
51+
}
52+
53+
return tag[1] === criterion
54+
}
4355

4456
// NIP-01: Basic protocol flow description
4557

@@ -84,7 +96,7 @@ export const isEventMatchingFilter =
8496
Object.entries(filter)
8597
.filter(([key, criteria]) => isGenericTagQuery(key) && Array.isArray(criteria))
8698
.some(([key, criteria]) => {
87-
return !event.tags.some((tag) => tag[0] === key[1] && criteria.includes(tag[1]))
99+
return !event.tags.some((tag) => criteria.some((criterion) => isMatchingGenericTagCriterion(key, criterion)(tag)))
88100
})
89101
) {
90102
return false

test/unit/repositories/event-repository.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,28 @@ describe('EventRepository', () => {
320320
})
321321
})
322322

323+
describe('#g', () => {
324+
it('selects geohash tags by prefix when criterion ends with wildcard', () => {
325+
const filters = [{ '#g': ['u4pruyd*'] }]
326+
327+
const query = repository.findByFilters(filters).toString()
328+
329+
expect(query).to.equal(
330+
'select "events".* from "events" left join "event_tags" on "events"."event_id" = "event_tags"."event_id" where (event_tags.tag_name = \'g\' AND event_tags.tag_value LIKE \'u4pruyd%\') order by "event_created_at" asc, "event_id" asc limit 500',
331+
)
332+
})
333+
334+
it('keeps geohash tags exact when criterion has no wildcard', () => {
335+
const filters = [{ '#g': ['u4pruyd'] }]
336+
337+
const query = repository.findByFilters(filters).toString()
338+
339+
expect(query).to.equal(
340+
'select "events".* from "events" left join "event_tags" on "events"."event_id" = "event_tags"."event_id" where (event_tags.tag_name = \'g\' AND event_tags.tag_value = \'u4pruyd\') order by "event_created_at" asc, "event_id" asc limit 500',
341+
)
342+
})
343+
})
344+
323345
describe('#p', () => {
324346
it('selects no events given empty list of #p tags', () => {
325347
const filters = [{ '#p': [] }]

test/unit/utils/event.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,33 @@ describe('NIP-12', () => {
302302
expect(isEventMatchingFilter({ '#r': ['something else'] })(event)).to.be.false
303303
})
304304
})
305+
306+
describe('#g filter', () => {
307+
beforeEach(() => {
308+
event = {
309+
id: 'cf8de9db67a1d7203512d1d81e6190f5e53abfdc0ac90275f67172b65a5b09a0',
310+
pubkey: 'e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7',
311+
created_at: 1645030752,
312+
kind: 1,
313+
tags: [['g', 'u4pruydqqvj']],
314+
content: 'g',
315+
sig: '53d12018d036092794366283eca36df4e0cabd014b6e91bbf684c8bb9bbbe9dedafa77b6b928587e11e05e036227598dded8713e8da17d55076e12242b361542',
316+
}
317+
})
318+
319+
it('returns true if #g filter contains a matching geohash prefix wildcard', () => {
320+
expect(isEventMatchingFilter({ '#g': ['u4pruyd*'] })(event)).to.be.true
321+
})
322+
323+
it('returns false if #g filter contains a non-matching geohash prefix wildcard', () => {
324+
expect(isEventMatchingFilter({ '#g': ['u4pruz*'] })(event)).to.be.false
325+
})
326+
327+
it('keeps #g filter exact when criterion has no wildcard', () => {
328+
expect(isEventMatchingFilter({ '#g': ['u4pruyd'] })(event)).to.be.false
329+
expect(isEventMatchingFilter({ '#g': ['u4pruydqqvj'] })(event)).to.be.true
330+
})
331+
})
305332
})
306333

307334
describe('NIP-16', () => {

0 commit comments

Comments
 (0)