Change event search

- case insensitive regex
- use object type for filters param
- update API docs
This commit is contained in:
ssmucny 2023-04-30 17:30:27 -04:00
parent 5f6d708e77
commit a12152cd4f
3 changed files with 23 additions and 12 deletions

View File

@ -55,10 +55,13 @@ export const paramDef = {
type: 'array', type: 'array',
nullable: true, nullable: true,
description: 'list of string -> [string] that filters events based on metadata. Each item in filters is applied as an AND', description: 'list of string -> [string] that filters events based on metadata. Each item in filters is applied as an AND',
prefixItems: [ items: {
{ type: 'string', description: 'The property of metadata to be filtered' }, type: 'object',
{ type: 'array', items: { type: 'string', nullable: true, description: 'The values to match the metadata against. Each item in this array is applied as an OR. Include null to indicate match on missing metadata' } }, properties: {
], key: { type: 'string', description: 'the metadata property to filter on.' },
values: { type: 'array', items: { type: 'string', nullable: true }, description: 'The values to match the metadata against (case insensitive regex). Each item in this array is applied as an OR. Include null to indicate match on missing metadata' },
},
},
}, },
sortBy: { type: 'string', nullable: true, default: 'startDate', enum: ['startDate', 'createdAt'] }, sortBy: { type: 'string', nullable: true, default: 'startDate', enum: ['startDate', 'createdAt'] },
}, },
@ -84,7 +87,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.unavailable); throw new ApiError(meta.errors.unavailable);
} }
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); const queryRunner = this.notesRepository.queryRunner;
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note', queryRunner), ps.sinceId, ps.untilId);
if (ps.users) { if (ps.users) {
if (ps.users.length < 1) throw new ApiError(meta.errors.invalidParam); if (ps.users.length < 1) throw new ApiError(meta.errors.invalidParam);
@ -103,18 +107,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
})); }));
} }
if (ps.filters) { if (ps.filters) {
const filters: [string, (string | null)[]][] = ps.filters; const filters = ps.filters;
filters.forEach(f => { filters.forEach(f => {
const filterKey = f[0]; if (!f.key || !f.values) throw new ApiError(meta.errors.invalidParam);
const filterValues = f[1]; const filterKey = f.key;
const filterValues = f.values as (string | null)[];
const matches = filterValues.filter(x => x !== null) as string[]; const matches = filterValues.filter(x => x !== null) as string[];
const hasNull = filterValues.length !== matches.length; const hasNull = filterValues.length !== matches.length;
if (matches.length < 1) throw new ApiError(meta.errors.invalidParam); if (matches.length < 1) throw new ApiError(meta.errors.invalidParam);
query.andWhere(new Brackets((qb) => { query.andWhere(new Brackets((qb) => {
qb.where('event.metadata ->> :key SIMILAR TO :values', { key: filterKey, values: `%(${ matches.map(sqlLikeEscape).map(m => m.trim()).filter(m => m.length).join('|') })%` }); // regex match metadata values case insensitive
qb.where('event.metadata ->> :key ~* :values', { key: filterKey, values: `(${ matches.map(m => m.trim()).filter(m => m.length).join('|') })` });
if (hasNull) { if (hasNull) {
qb.orWhere('event.metadata ->> :key IS NULL', { key: filterKey }); qb.orWhere('NOT (event.metadata ? :key)', { key: filterKey });
} }
})); }));
}); });
@ -143,6 +149,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (me) this.queryService.generateBlockedUserQuery(query, me); if (me) this.queryService.generateBlockedUserQuery(query, me);
if (ps.offset) query.skip(ps.offset); if (ps.offset) query.skip(ps.offset);
query.maxExecutionTime(250); // because we include regex expressions in where clause, defend against long running regex with timeout
const notes = await query.take(ps.limit).getMany(); const notes = await query.take(ps.limit).getMany();
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);

View File

@ -1774,7 +1774,10 @@ export type Endpoints = {
sinceDate?: number; sinceDate?: number;
untilDate?: number; untilDate?: number;
sortBy?: 'startDate' | 'craetedAt'; sortBy?: 'startDate' | 'craetedAt';
filters?: [string, (string | null)[]][]; filters?: {
key: string;
values: (string | null)[];
}[];
}; };
res: Note[]; res: Note[];
}; };

View File

@ -515,7 +515,7 @@ export type Endpoints = {
sinceDate?: number; sinceDate?: number;
untilDate?: number; untilDate?: number;
sortBy?: 'startDate' | 'craetedAt'; sortBy?: 'startDate' | 'craetedAt';
filters?: [string, (string | null)[]][]; filters?: { key: string, values: (string | null)[] }[];
}; res: Note[]; }; }; res: Note[]; };
'notes/reactions': { req: { noteId: Note['id']; type?: string | null; limit?: number; }; res: NoteReaction[]; }; 'notes/reactions': { req: { noteId: Note['id']; type?: string | null; limit?: number; }; res: NoteReaction[]; };
'notes/reactions/create': { req: { noteId: Note['id']; reaction: string; }; res: null; }; 'notes/reactions/create': { req: { noteId: Note['id']; reaction: string; }; res: null; };