diff --git a/src/service.test.ts b/src/service.test.ts index ea9e0d183..2f1205d23 100644 --- a/src/service.test.ts +++ b/src/service.test.ts @@ -303,6 +303,42 @@ await test('find', async (t) => { name: OBJECT, res: obj, }, + // Search functionality tests + { + name: POSTS, + params: { q: 'a' }, + res: [post1, post2, post3], + }, + { + name: POSTS, + params: { q: 'baz' }, + res: [post3], + }, + { + name: POSTS, + params: { q: 'c' }, + res: [post3], + }, + { + name: POSTS, + params: { q: 'foo' }, + res: [post1, post3], + }, + { + name: POSTS, + params: { q: 'bar' }, + res: [post1, post2], + }, + { + name: POSTS, + params: { q: 'nonexistent' }, + res: [], + }, + { + name: COMMENTS, + params: { q: 'a' }, + res: [comment1], + }, ] for (const tc of arr) { await t.test(`${tc.name} ${JSON.stringify(tc.params)}`, () => { diff --git a/src/service.ts b/src/service.ts index 2a607da02..86d9ddb8d 100644 --- a/src/service.ts +++ b/src/service.ts @@ -194,6 +194,14 @@ export class Service { return items } + // Handle full-text search with q parameter + if (query['q'] && typeof query['q'] === 'string') { + const searchTerm = query['q'].toLowerCase() + items = items.filter((item: Item) => { + return this.#searchInItem(item, searchTerm) + }) + } + // Convert query params to conditions const conds: [string, Condition, string | string[]][] = [] for (const [key, value] of Object.entries(query)) { @@ -208,7 +216,7 @@ export class Service { conds.push([field, op, value]) continue } - if (['_embed', '_sort', '_start', '_end', '_limit', '_page', '_per_page'].includes(key)) { + if (['_embed', '_sort', '_start', '_end', '_limit', '_page', '_per_page', 'q'].includes(key)) { continue } conds.push([key, Condition.default, value]) @@ -409,4 +417,44 @@ export class Service { await this.#db.write() return item } + + #searchInItem(item: Item, searchTerm: string): boolean { + for (const [, value] of Object.entries(item)) { + if (typeof value === 'string' && value.toLowerCase().includes(searchTerm)) { + return true + } + if (typeof value === 'object' && value !== null) { + if (this.#searchInObject(value as Record, searchTerm)) { + return true + } + } + } + return false + } + + #searchInObject(obj: Record, searchTerm: string): boolean { + for (const [, value] of Object.entries(obj)) { + if (typeof value === 'string' && value.toLowerCase().includes(searchTerm)) { + return true + } + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + if (this.#searchInObject(value as Record, searchTerm)) { + return true + } + } + if (Array.isArray(value)) { + for (const arrayItem of value) { + if (typeof arrayItem === 'string' && arrayItem.toLowerCase().includes(searchTerm)) { + return true + } + if (typeof arrayItem === 'object' && arrayItem !== null) { + if (this.#searchInObject(arrayItem as Record, searchTerm)) { + return true + } + } + } + } + } + return false + } }