feat: add search adapter for entity searches
- Implement SearchAdapter extending BaseAdapter - Support searching weapons, characters, and summons - Add filtering by element, rarity, proficiency, etc. - Include pagination support - Add comprehensive test coverage
This commit is contained in:
parent
51c30edc50
commit
ece44a54a3
3 changed files with 745 additions and 0 deletions
427
src/lib/api/adapters/__tests__/search.adapter.test.ts
Normal file
427
src/lib/api/adapters/__tests__/search.adapter.test.ts
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
/**
|
||||||
|
* Tests for SearchAdapter
|
||||||
|
*
|
||||||
|
* These tests verify search functionality including filtering,
|
||||||
|
* pagination, and proper request/response handling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { SearchAdapter } from '../search.adapter'
|
||||||
|
import type { SearchParams, SearchResponse } from '../search.adapter'
|
||||||
|
|
||||||
|
describe('SearchAdapter', () => {
|
||||||
|
let adapter: SearchAdapter
|
||||||
|
let originalFetch: typeof global.fetch
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalFetch = global.fetch
|
||||||
|
adapter = new SearchAdapter({
|
||||||
|
baseURL: 'https://api.example.com'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
adapter.cancelAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searchAll', () => {
|
||||||
|
it('should search across all entity types', async () => {
|
||||||
|
const mockResponse: SearchResponse = {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
granblueId: 'weapon_1',
|
||||||
|
name: { en: 'Bahamut Sword', ja: 'バハムートソード' },
|
||||||
|
element: 1,
|
||||||
|
rarity: 5,
|
||||||
|
searchableType: 'Weapon',
|
||||||
|
imageUrl: 'https://example.com/weapon1.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
granblueId: 'character_1',
|
||||||
|
name: { en: 'Bahamut', ja: 'バハムート' },
|
||||||
|
element: 6,
|
||||||
|
rarity: 5,
|
||||||
|
searchableType: 'Character',
|
||||||
|
imageUrl: 'https://example.com/character1.jpg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
meta: {
|
||||||
|
count: 2,
|
||||||
|
page: 1,
|
||||||
|
perPage: 20,
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
granblue_id: 'weapon_1',
|
||||||
|
name: { en: 'Bahamut Sword', ja: 'バハムートソード' },
|
||||||
|
element: 1,
|
||||||
|
rarity: 5,
|
||||||
|
searchable_type: 'Weapon',
|
||||||
|
image_url: 'https://example.com/weapon1.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
granblue_id: 'character_1',
|
||||||
|
name: { en: 'Bahamut', ja: 'バハムート' },
|
||||||
|
element: 6,
|
||||||
|
rarity: 5,
|
||||||
|
searchable_type: 'Character',
|
||||||
|
image_url: 'https://example.com/character1.jpg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
page: 1,
|
||||||
|
total_pages: 1,
|
||||||
|
meta: {
|
||||||
|
count: 2,
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
total_pages: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'bahamut',
|
||||||
|
locale: 'en',
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await adapter.searchAll(params)
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/search/all',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'omit',
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 1,
|
||||||
|
query: 'bahamut'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResponse)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include filters when provided', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'sword',
|
||||||
|
filters: {
|
||||||
|
element: [1, 2],
|
||||||
|
rarity: [5],
|
||||||
|
extra: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.searchAll(params)
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 1,
|
||||||
|
query: 'sword',
|
||||||
|
filters: {
|
||||||
|
element: [1, 2],
|
||||||
|
rarity: [5],
|
||||||
|
extra: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle exclude parameter', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'test',
|
||||||
|
exclude: ['1', '2', '3']
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.searchAll(params)
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 1,
|
||||||
|
query: 'test',
|
||||||
|
exclude: ['1', '2', '3']
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should cache results when query is provided', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [{ id: '1' }] })
|
||||||
|
})
|
||||||
|
|
||||||
|
// First call
|
||||||
|
await adapter.searchAll({ query: 'test' })
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Second call should use cache
|
||||||
|
await adapter.searchAll({ query: 'test' })
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not cache results when query is empty', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
// First call
|
||||||
|
await adapter.searchAll({})
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Second call should not use cache
|
||||||
|
await adapter.searchAll({})
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searchWeapons', () => {
|
||||||
|
it('should search for weapons with appropriate filters', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'sword',
|
||||||
|
filters: {
|
||||||
|
element: [1],
|
||||||
|
proficiency1: [2],
|
||||||
|
extra: false
|
||||||
|
},
|
||||||
|
per: 50
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.searchWeapons(params)
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/search/weapons',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'omit',
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 1,
|
||||||
|
per: 50,
|
||||||
|
query: 'sword',
|
||||||
|
filters: {
|
||||||
|
element: [1],
|
||||||
|
proficiency1: [2],
|
||||||
|
extra: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not include character-specific filters', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'test',
|
||||||
|
filters: {
|
||||||
|
proficiency2: [1], // Character-only filter
|
||||||
|
subaura: true // Summon-only filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.searchWeapons(params)
|
||||||
|
|
||||||
|
const calledBody = JSON.parse((global.fetch as any).mock.calls[0][1].body)
|
||||||
|
expect(calledBody.filters).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searchCharacters', () => {
|
||||||
|
it('should search for characters with appropriate filters', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'katalina',
|
||||||
|
filters: {
|
||||||
|
element: [2],
|
||||||
|
proficiency1: [1],
|
||||||
|
proficiency2: [3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.searchCharacters(params)
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/search/characters',
|
||||||
|
expect.objectContaining({
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 1,
|
||||||
|
query: 'katalina',
|
||||||
|
filters: {
|
||||||
|
element: [2],
|
||||||
|
proficiency1: [1],
|
||||||
|
proficiency2: [3]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('searchSummons', () => {
|
||||||
|
it('should search for summons with appropriate filters', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const params: SearchParams = {
|
||||||
|
query: 'bahamut',
|
||||||
|
filters: {
|
||||||
|
element: [6],
|
||||||
|
rarity: [5],
|
||||||
|
subaura: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await adapter.searchSummons(params)
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/search/summons',
|
||||||
|
expect.objectContaining({
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 1,
|
||||||
|
query: 'bahamut',
|
||||||
|
filters: {
|
||||||
|
element: [6],
|
||||||
|
rarity: [5],
|
||||||
|
subaura: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('clearSearchCache', () => {
|
||||||
|
it('should clear cached search results', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ results: [{ id: '1' }] })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cache a search
|
||||||
|
await adapter.searchAll({ query: 'test' })
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
adapter.clearSearchCache()
|
||||||
|
|
||||||
|
// Should fetch again
|
||||||
|
await adapter.searchAll({ query: 'test' })
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should handle server errors', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
json: async () => ({ error: 'Server error' })
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(adapter.searchAll({ query: 'test' })).rejects.toMatchObject({
|
||||||
|
code: 'SERVER_ERROR',
|
||||||
|
status: 500
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle network errors', async () => {
|
||||||
|
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||||
|
|
||||||
|
await expect(adapter.searchAll({ query: 'test' })).rejects.toMatchObject({
|
||||||
|
code: 'UNKNOWN_ERROR'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('pagination', () => {
|
||||||
|
it('should handle pagination parameters', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
results: [],
|
||||||
|
page: 2,
|
||||||
|
total_pages: 5,
|
||||||
|
meta: {
|
||||||
|
count: 0,
|
||||||
|
page: 2,
|
||||||
|
per_page: 20,
|
||||||
|
total_pages: 5
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.searchAll({
|
||||||
|
query: 'test',
|
||||||
|
page: 2,
|
||||||
|
per: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: JSON.stringify({
|
||||||
|
locale: 'en',
|
||||||
|
page: 2,
|
||||||
|
per: 20,
|
||||||
|
query: 'test'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.page).toBe(2)
|
||||||
|
expect(result.totalPages).toBe(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
22
src/lib/api/adapters/index.ts
Normal file
22
src/lib/api/adapters/index.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* Main export file for the adapter system
|
||||||
|
*
|
||||||
|
* This module re-exports all public APIs from the adapter system,
|
||||||
|
* providing a single entry point for consumers.
|
||||||
|
*
|
||||||
|
* @module adapters
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Core exports
|
||||||
|
export { BaseAdapter } from './base.adapter'
|
||||||
|
export * from './types'
|
||||||
|
export * from './errors'
|
||||||
|
|
||||||
|
// Resource-specific adapters
|
||||||
|
export { SearchAdapter, searchAdapter } from './search.adapter'
|
||||||
|
export type { SearchParams, SearchResult, SearchResponse } from './search.adapter'
|
||||||
|
// export { PartyAdapter } from './party.adapter'
|
||||||
|
// export { GridAdapter } from './grid.adapter'
|
||||||
|
|
||||||
|
// Reactive resources using Svelte 5 runes
|
||||||
|
export * from './resources'
|
||||||
296
src/lib/api/adapters/search.adapter.ts
Normal file
296
src/lib/api/adapters/search.adapter.ts
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
/**
|
||||||
|
* Search Adapter for Entity Search Operations
|
||||||
|
*
|
||||||
|
* Handles all search-related API calls for weapons, characters, and summons.
|
||||||
|
* Provides unified interface with automatic transformation and error handling.
|
||||||
|
*
|
||||||
|
* @module adapters/search
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseAdapter } from './base.adapter'
|
||||||
|
import type { AdapterOptions, SearchFilters } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search parameters for entity queries
|
||||||
|
* Used across all search methods
|
||||||
|
*/
|
||||||
|
export interface SearchParams {
|
||||||
|
/** Search query string */
|
||||||
|
query?: string
|
||||||
|
/** Locale for search results */
|
||||||
|
locale?: 'en' | 'ja'
|
||||||
|
/** Entity IDs to exclude from results */
|
||||||
|
exclude?: string[]
|
||||||
|
/** Page number for pagination */
|
||||||
|
page?: number
|
||||||
|
/** Number of results per page */
|
||||||
|
per?: number
|
||||||
|
/** Search filters */
|
||||||
|
filters?: SearchFilters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Individual search result item
|
||||||
|
* Represents a weapon, character, or summon
|
||||||
|
*/
|
||||||
|
export interface SearchResult {
|
||||||
|
/** Unique entity ID */
|
||||||
|
id: string
|
||||||
|
/** Granblue game ID */
|
||||||
|
granblueId: string
|
||||||
|
/** Localized names */
|
||||||
|
name: {
|
||||||
|
en?: string
|
||||||
|
ja?: string
|
||||||
|
}
|
||||||
|
/** Element type (1-6 for different elements) */
|
||||||
|
element?: number
|
||||||
|
/** Rarity level */
|
||||||
|
rarity?: number
|
||||||
|
/** Weapon/Character proficiency */
|
||||||
|
proficiency?: number
|
||||||
|
/** Series ID for categorization */
|
||||||
|
series?: number
|
||||||
|
/** URL for entity image */
|
||||||
|
imageUrl?: string
|
||||||
|
/** Type of entity */
|
||||||
|
searchableType: 'Weapon' | 'Character' | 'Summon'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search API response structure
|
||||||
|
* Contains results and pagination metadata
|
||||||
|
*/
|
||||||
|
export interface SearchResponse {
|
||||||
|
/** Array of search results */
|
||||||
|
results: SearchResult[]
|
||||||
|
/** Total number of results */
|
||||||
|
total?: number
|
||||||
|
/** Current page number */
|
||||||
|
page?: number
|
||||||
|
/** Total number of pages */
|
||||||
|
totalPages?: number
|
||||||
|
/** Pagination metadata */
|
||||||
|
meta?: {
|
||||||
|
count: number
|
||||||
|
page: number
|
||||||
|
perPage: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for search-related API operations
|
||||||
|
* Handles entity search with filtering, pagination, and caching
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const searchAdapter = new SearchAdapter()
|
||||||
|
*
|
||||||
|
* // Search for fire weapons
|
||||||
|
* const weapons = await searchAdapter.searchWeapons({
|
||||||
|
* query: 'sword',
|
||||||
|
* filters: { element: [1] }
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* // Search across all entity types
|
||||||
|
* const results = await searchAdapter.searchAll({
|
||||||
|
* query: 'bahamut',
|
||||||
|
* page: 1
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class SearchAdapter extends BaseAdapter {
|
||||||
|
/**
|
||||||
|
* Creates a new SearchAdapter instance
|
||||||
|
*
|
||||||
|
* @param options - Adapter configuration options
|
||||||
|
*/
|
||||||
|
constructor(options?: AdapterOptions) {
|
||||||
|
super({
|
||||||
|
...options,
|
||||||
|
// Search endpoints don't use credentials to avoid CORS issues
|
||||||
|
// This is handled per-request instead
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds search request body from parameters
|
||||||
|
* Handles filtering logic and defaults
|
||||||
|
*
|
||||||
|
* @param params - Search parameters
|
||||||
|
* @param includeFilters - Which filters to include
|
||||||
|
* @returns Request body object
|
||||||
|
*/
|
||||||
|
private buildSearchBody(
|
||||||
|
params: SearchParams,
|
||||||
|
includeFilters: {
|
||||||
|
element?: boolean
|
||||||
|
rarity?: boolean
|
||||||
|
proficiency1?: boolean
|
||||||
|
proficiency2?: boolean
|
||||||
|
series?: boolean
|
||||||
|
extra?: boolean
|
||||||
|
subaura?: boolean
|
||||||
|
} = {}
|
||||||
|
): any {
|
||||||
|
const body: any = {
|
||||||
|
locale: params.locale || 'en',
|
||||||
|
page: params.page || 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include per if specified
|
||||||
|
if (params.per) {
|
||||||
|
body.per = params.per
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include query if provided and not empty
|
||||||
|
if (params.query) {
|
||||||
|
body.query = params.query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include exclude if provided
|
||||||
|
if (params.exclude?.length) {
|
||||||
|
body.exclude = params.exclude
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filters based on what's allowed for this search type
|
||||||
|
if (params.filters) {
|
||||||
|
const filters: any = {}
|
||||||
|
|
||||||
|
if (includeFilters.element && params.filters.element?.length) {
|
||||||
|
filters.element = params.filters.element
|
||||||
|
}
|
||||||
|
if (includeFilters.rarity && params.filters.rarity?.length) {
|
||||||
|
filters.rarity = params.filters.rarity
|
||||||
|
}
|
||||||
|
if (includeFilters.proficiency1 && params.filters.proficiency1?.length) {
|
||||||
|
filters.proficiency1 = params.filters.proficiency1
|
||||||
|
}
|
||||||
|
if (includeFilters.proficiency2 && params.filters.proficiency2?.length) {
|
||||||
|
filters.proficiency2 = params.filters.proficiency2
|
||||||
|
}
|
||||||
|
if (includeFilters.series && params.filters.series?.length) {
|
||||||
|
filters.series = params.filters.series
|
||||||
|
}
|
||||||
|
if (includeFilters.extra && params.filters.extra !== undefined) {
|
||||||
|
filters.extra = params.filters.extra
|
||||||
|
}
|
||||||
|
if (includeFilters.subaura && params.filters.subaura !== undefined) {
|
||||||
|
filters.subaura = params.filters.subaura
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(filters).length > 0) {
|
||||||
|
body.filters = filters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches across all entity types (weapons, characters, summons)
|
||||||
|
*
|
||||||
|
* @param params - Search parameters
|
||||||
|
* @returns Promise resolving to search results
|
||||||
|
*/
|
||||||
|
async searchAll(params: SearchParams = {}): Promise<SearchResponse> {
|
||||||
|
const body = this.buildSearchBody(params, {
|
||||||
|
element: true,
|
||||||
|
rarity: true,
|
||||||
|
proficiency1: true,
|
||||||
|
proficiency2: true,
|
||||||
|
series: true,
|
||||||
|
extra: true,
|
||||||
|
subaura: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Search endpoints don't use credentials to avoid CORS
|
||||||
|
return this.request<SearchResponse>('/search/all', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: 'omit',
|
||||||
|
// Cache search results for 5 minutes by default
|
||||||
|
cache: params.query ? 300000 : 0 // Don't cache empty searches
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for weapons with specific filters
|
||||||
|
*
|
||||||
|
* @param params - Search parameters
|
||||||
|
* @returns Promise resolving to weapon search results
|
||||||
|
*/
|
||||||
|
async searchWeapons(params: SearchParams = {}): Promise<SearchResponse> {
|
||||||
|
const body = this.buildSearchBody(params, {
|
||||||
|
element: true,
|
||||||
|
rarity: true,
|
||||||
|
proficiency1: true,
|
||||||
|
extra: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.request<SearchResponse>('/search/weapons', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: params.query ? 300000 : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for characters with specific filters
|
||||||
|
*
|
||||||
|
* @param params - Search parameters
|
||||||
|
* @returns Promise resolving to character search results
|
||||||
|
*/
|
||||||
|
async searchCharacters(params: SearchParams = {}): Promise<SearchResponse> {
|
||||||
|
const body = this.buildSearchBody(params, {
|
||||||
|
element: true,
|
||||||
|
rarity: true,
|
||||||
|
proficiency1: true,
|
||||||
|
proficiency2: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.request<SearchResponse>('/search/characters', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: params.query ? 300000 : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for summons with specific filters
|
||||||
|
*
|
||||||
|
* @param params - Search parameters
|
||||||
|
* @returns Promise resolving to summon search results
|
||||||
|
*/
|
||||||
|
async searchSummons(params: SearchParams = {}): Promise<SearchResponse> {
|
||||||
|
const body = this.buildSearchBody(params, {
|
||||||
|
element: true,
|
||||||
|
rarity: true,
|
||||||
|
subaura: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.request<SearchResponse>('/search/summons', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: params.query ? 300000 : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all cached search results
|
||||||
|
* Useful when entity data has been updated
|
||||||
|
*/
|
||||||
|
clearSearchCache(): void {
|
||||||
|
this.clearCache('search')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default singleton instance for search operations
|
||||||
|
* Use this for most search needs unless you need custom configuration
|
||||||
|
*/
|
||||||
|
export const searchAdapter = new SearchAdapter()
|
||||||
Loading…
Reference in a new issue