@youdotcom-oss/api 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -36
- package/bin/cli.js +233 -400
- package/package.json +1 -1
- package/src/cli.ts +92 -21
- package/src/contents/contents.schemas.ts +8 -7
- package/src/contents/tests/contents.request.spec.ts +109 -0
- package/src/contents/tests/contents.schema-validation.spec.ts +75 -0
- package/src/deep-search/deep-search.schemas.ts +48 -0
- package/src/deep-search/deep-search.utils.ts +79 -0
- package/src/deep-search/tests/deep-search.request.spec.ts +109 -0
- package/src/deep-search/tests/deep-search.schema-validation.spec.ts +71 -0
- package/src/deep-search/tests/deep-search.utils.docker.ts +139 -0
- package/src/main.ts +4 -3
- package/src/search/search.schemas.ts +65 -6
- package/src/search/search.utils.ts +6 -28
- package/src/search/tests/search.request.spec.ts +122 -0
- package/src/search/tests/search.schema-validation.spec.ts +152 -0
- package/src/search/tests/{search.utils.spec.ts → search.utils.docker.ts} +0 -10
- package/src/shared/api.constants.ts +1 -1
- package/src/shared/check-response-for-errors.ts +1 -1
- package/src/shared/command-runner.ts +95 -0
- package/src/shared/dry-run-utils.ts +141 -0
- package/src/shared/tests/command-runner.spec.ts +210 -0
- package/src/shared/use-get-user-agents.ts +1 -1
- package/src/commands/contents.ts +0 -52
- package/src/commands/express.ts +0 -52
- package/src/commands/search.ts +0 -52
- package/src/express/express.schemas.ts +0 -85
- package/src/express/express.utils.ts +0 -113
- package/src/express/tests/express.utils.spec.ts +0 -83
- /package/src/contents/tests/{contents.utils.spec.ts → contents.utils.docker.ts} +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { callDeepSearch } from '../deep-search.utils.ts'
|
|
3
|
+
|
|
4
|
+
const getUserAgent = () => 'API/test (You.com;TEST)'
|
|
5
|
+
|
|
6
|
+
describe('callDeepSearch', () => {
|
|
7
|
+
test(
|
|
8
|
+
'returns valid response structure for basic query',
|
|
9
|
+
async () => {
|
|
10
|
+
const result = await callDeepSearch({
|
|
11
|
+
deepSearchQuery: {
|
|
12
|
+
query: 'What is TypeScript?',
|
|
13
|
+
search_effort: 'low',
|
|
14
|
+
},
|
|
15
|
+
getUserAgent,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(result).toHaveProperty('answer')
|
|
19
|
+
expect(result).toHaveProperty('results')
|
|
20
|
+
expect(typeof result.answer).toBe('string')
|
|
21
|
+
expect(Array.isArray(result.results)).toBe(true)
|
|
22
|
+
expect(result.answer.length).toBeGreaterThan(0)
|
|
23
|
+
},
|
|
24
|
+
{ retry: 2 },
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
test(
|
|
28
|
+
'handles medium search effort',
|
|
29
|
+
async () => {
|
|
30
|
+
const result = await callDeepSearch({
|
|
31
|
+
deepSearchQuery: {
|
|
32
|
+
query: 'Explain REST API principles',
|
|
33
|
+
search_effort: 'medium',
|
|
34
|
+
},
|
|
35
|
+
getUserAgent,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(result).toHaveProperty('answer')
|
|
39
|
+
expect(result).toHaveProperty('results')
|
|
40
|
+
expect(typeof result.answer).toBe('string')
|
|
41
|
+
expect(Array.isArray(result.results)).toBe(true)
|
|
42
|
+
},
|
|
43
|
+
{ retry: 2 },
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
test(
|
|
47
|
+
'validates response schema with sources',
|
|
48
|
+
async () => {
|
|
49
|
+
const result = await callDeepSearch({
|
|
50
|
+
deepSearchQuery: {
|
|
51
|
+
query: 'What are the benefits of microservices?',
|
|
52
|
+
search_effort: 'low',
|
|
53
|
+
},
|
|
54
|
+
getUserAgent,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Test that results have required properties
|
|
58
|
+
expect(result.results.length).toBeGreaterThan(0)
|
|
59
|
+
|
|
60
|
+
const source = result.results[0]
|
|
61
|
+
expect(source).toBeDefined()
|
|
62
|
+
expect(source).toHaveProperty('url')
|
|
63
|
+
expect(source).toHaveProperty('title')
|
|
64
|
+
expect(source).toHaveProperty('snippets')
|
|
65
|
+
expect(typeof source?.url).toBe('string')
|
|
66
|
+
expect(typeof source?.title).toBe('string')
|
|
67
|
+
expect(Array.isArray(source?.snippets)).toBe(true)
|
|
68
|
+
},
|
|
69
|
+
{ retry: 2 },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
test(
|
|
73
|
+
'answer contains markdown with inline citations',
|
|
74
|
+
async () => {
|
|
75
|
+
const result = await callDeepSearch({
|
|
76
|
+
deepSearchQuery: {
|
|
77
|
+
query: 'What is JWT authentication?',
|
|
78
|
+
search_effort: 'low',
|
|
79
|
+
},
|
|
80
|
+
getUserAgent,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Answer should be non-empty markdown string
|
|
84
|
+
expect(typeof result.answer).toBe('string')
|
|
85
|
+
expect(result.answer.length).toBeGreaterThan(0)
|
|
86
|
+
|
|
87
|
+
// Answer typically contains citations in the format [1], [2], etc.
|
|
88
|
+
// This is a soft check - citations may or may not be present
|
|
89
|
+
if (result.answer.includes('[')) {
|
|
90
|
+
expect(result.answer).toMatch(/\[\d+\]/)
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{ retry: 2 },
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
test(
|
|
97
|
+
'handles complex multi-part questions',
|
|
98
|
+
async () => {
|
|
99
|
+
const result = await callDeepSearch({
|
|
100
|
+
deepSearchQuery: {
|
|
101
|
+
query: 'What is GraphQL and how does it differ from REST?',
|
|
102
|
+
search_effort: 'low',
|
|
103
|
+
},
|
|
104
|
+
getUserAgent,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
expect(result).toHaveProperty('answer')
|
|
108
|
+
expect(result).toHaveProperty('results')
|
|
109
|
+
expect(result.results.length).toBeGreaterThan(0)
|
|
110
|
+
|
|
111
|
+
// Complex questions should have substantial answers
|
|
112
|
+
expect(result.answer.length).toBeGreaterThan(100)
|
|
113
|
+
},
|
|
114
|
+
{ retry: 2 },
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
test(
|
|
118
|
+
'sources include relevant snippets',
|
|
119
|
+
async () => {
|
|
120
|
+
const result = await callDeepSearch({
|
|
121
|
+
deepSearchQuery: {
|
|
122
|
+
query: 'What is Docker containerization?',
|
|
123
|
+
search_effort: 'low',
|
|
124
|
+
},
|
|
125
|
+
getUserAgent,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Check that at least one source has snippets
|
|
129
|
+
const sourcesWithSnippets = result.results.filter((source) => source.snippets.length > 0)
|
|
130
|
+
expect(sourcesWithSnippets.length).toBeGreaterThan(0)
|
|
131
|
+
|
|
132
|
+
// Snippets should be non-empty strings
|
|
133
|
+
const firstSource = sourcesWithSnippets[0]
|
|
134
|
+
expect(firstSource).toBeDefined()
|
|
135
|
+
expect(firstSource?.snippets[0]?.length).toBeGreaterThan(0)
|
|
136
|
+
},
|
|
137
|
+
{ retry: 2 },
|
|
138
|
+
)
|
|
139
|
+
})
|
package/src/main.ts
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
// Contents
|
|
11
11
|
export * from './contents/contents.schemas.ts'
|
|
12
12
|
export * from './contents/contents.utils.ts'
|
|
13
|
-
//
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
13
|
+
// Deep-Search
|
|
14
|
+
export * from './deep-search/deep-search.schemas.ts'
|
|
15
|
+
export * from './deep-search/deep-search.utils.ts'
|
|
16
16
|
// Search
|
|
17
17
|
export * from './search/search.schemas.ts'
|
|
18
18
|
export * from './search/search.utils.ts'
|
|
19
19
|
// Shared
|
|
20
20
|
export * from './shared/api.constants.ts'
|
|
21
21
|
export * from './shared/api.types.ts'
|
|
22
|
+
export * from './shared/dry-run-utils.ts'
|
|
22
23
|
export * from './shared/generate-error-report-link.ts'
|
|
@@ -1,7 +1,70 @@
|
|
|
1
1
|
import * as z from 'zod'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Language codes supported by You.com Search API (BCP 47 format)
|
|
5
|
+
* Based on OpenAPI spec: https://you.com/specs/openapi_search_v1.yaml
|
|
6
|
+
*/
|
|
7
|
+
export const LanguageSchema = z.enum([
|
|
8
|
+
'AR',
|
|
9
|
+
'EU',
|
|
10
|
+
'BN',
|
|
11
|
+
'BG',
|
|
12
|
+
'CA',
|
|
13
|
+
'ZH-HANS',
|
|
14
|
+
'ZH-HANT',
|
|
15
|
+
'HR',
|
|
16
|
+
'CS',
|
|
17
|
+
'DA',
|
|
18
|
+
'NL',
|
|
19
|
+
'EN',
|
|
20
|
+
'EN-GB',
|
|
21
|
+
'ET',
|
|
22
|
+
'FI',
|
|
23
|
+
'FR',
|
|
24
|
+
'GL',
|
|
25
|
+
'DE',
|
|
26
|
+
'EL',
|
|
27
|
+
'GU',
|
|
28
|
+
'HE',
|
|
29
|
+
'HI',
|
|
30
|
+
'HU',
|
|
31
|
+
'IS',
|
|
32
|
+
'IT',
|
|
33
|
+
'JP',
|
|
34
|
+
'KN',
|
|
35
|
+
'KO',
|
|
36
|
+
'LV',
|
|
37
|
+
'LT',
|
|
38
|
+
'MS',
|
|
39
|
+
'ML',
|
|
40
|
+
'MR',
|
|
41
|
+
'NB',
|
|
42
|
+
'PL',
|
|
43
|
+
'PT-BR',
|
|
44
|
+
'PT-PT',
|
|
45
|
+
'PA',
|
|
46
|
+
'RO',
|
|
47
|
+
'RU',
|
|
48
|
+
'SR',
|
|
49
|
+
'SK',
|
|
50
|
+
'SL',
|
|
51
|
+
'ES',
|
|
52
|
+
'SV',
|
|
53
|
+
'TA',
|
|
54
|
+
'TE',
|
|
55
|
+
'TH',
|
|
56
|
+
'TR',
|
|
57
|
+
'UK',
|
|
58
|
+
'VI',
|
|
59
|
+
])
|
|
60
|
+
|
|
3
61
|
export const SearchQuerySchema = z.object({
|
|
4
|
-
query: z
|
|
62
|
+
query: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(1, 'Query is required')
|
|
65
|
+
.describe(
|
|
66
|
+
'Search query. Supports operators: site:domain.com (domain filter), filetype:pdf (file type), +term (include), -term (exclude), AND/OR/NOT (boolean logic), lang:en (language). Example: "machine learning (Python OR PyTorch) -TensorFlow filetype:pdf"',
|
|
67
|
+
),
|
|
5
68
|
count: z.number().int().min(1).max(100).optional().describe('Max results per section'),
|
|
6
69
|
freshness: z.string().optional().describe('day/week/month/year or YYYY-MM-DDtoYYYY-MM-DD'),
|
|
7
70
|
offset: z.number().int().min(0).max(9).optional().describe('Pagination offset'),
|
|
@@ -32,6 +95,7 @@ export const SearchQuerySchema = z.object({
|
|
|
32
95
|
'CN',
|
|
33
96
|
'PL',
|
|
34
97
|
'PT',
|
|
98
|
+
'PT-BR',
|
|
35
99
|
'PH',
|
|
36
100
|
'RU',
|
|
37
101
|
'SA',
|
|
@@ -47,11 +111,6 @@ export const SearchQuerySchema = z.object({
|
|
|
47
111
|
.optional()
|
|
48
112
|
.describe('Country code'),
|
|
49
113
|
safesearch: z.enum(['off', 'moderate', 'strict']).optional().describe('Filter level'),
|
|
50
|
-
site: z.string().optional().describe('Specific domain'),
|
|
51
|
-
fileType: z.string().optional().describe('File type'),
|
|
52
|
-
language: z.string().optional().describe('ISO 639-1 language code'),
|
|
53
|
-
excludeTerms: z.string().optional().describe('Terms to exclude (pipe-separated)'),
|
|
54
|
-
exactTerms: z.string().optional().describe('Exact terms (pipe-separated)'),
|
|
55
114
|
livecrawl: z.enum(['web', 'news', 'all']).optional().describe('Live-crawl sections for full content'),
|
|
56
115
|
livecrawl_formats: z.enum(['html', 'markdown']).optional().describe('Format for crawled content'),
|
|
57
116
|
})
|
|
@@ -5,7 +5,7 @@ import { type SearchQuery, SearchResponseSchema } from './search.schemas.ts'
|
|
|
5
5
|
|
|
6
6
|
export const fetchSearchResults = async ({
|
|
7
7
|
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
8
|
-
searchQuery
|
|
8
|
+
searchQuery,
|
|
9
9
|
getUserAgent,
|
|
10
10
|
}: {
|
|
11
11
|
searchQuery: SearchQuery
|
|
@@ -16,33 +16,11 @@ export const fetchSearchResults = async ({
|
|
|
16
16
|
|
|
17
17
|
const searchParams = new URLSearchParams()
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
const searchQuery
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (exactTerms && excludeTerms) {
|
|
25
|
-
throw new Error('Cannot specify both exactTerms and excludeTerms - please use only one')
|
|
26
|
-
}
|
|
27
|
-
exactTerms &&
|
|
28
|
-
searchQuery.push(
|
|
29
|
-
exactTerms
|
|
30
|
-
.split('|')
|
|
31
|
-
.map((term) => `+${term}`)
|
|
32
|
-
.join(' AND '),
|
|
33
|
-
)
|
|
34
|
-
excludeTerms &&
|
|
35
|
-
searchQuery.push(
|
|
36
|
-
excludeTerms
|
|
37
|
-
.split('|')
|
|
38
|
-
.map((term) => `-${term}`)
|
|
39
|
-
.join(' AND '),
|
|
40
|
-
)
|
|
41
|
-
searchParams.append('query', searchQuery.join(' '))
|
|
42
|
-
|
|
43
|
-
// Append additional advanced Params
|
|
44
|
-
for (const [name, value] of Object.entries(rest)) {
|
|
45
|
-
if (value) searchParams.append(name, `${value}`)
|
|
19
|
+
// Append all query parameters
|
|
20
|
+
for (const [name, value] of Object.entries(searchQuery)) {
|
|
21
|
+
if (value !== undefined && value !== null) {
|
|
22
|
+
searchParams.append(name, `${value}`)
|
|
23
|
+
}
|
|
46
24
|
}
|
|
47
25
|
|
|
48
26
|
url.search = searchParams.toString()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { SEARCH_API_URL } from '../../shared/api.constants.ts'
|
|
3
|
+
import { buildSearchRequest } from '../../shared/dry-run-utils.ts'
|
|
4
|
+
|
|
5
|
+
describe('buildSearchRequest', () => {
|
|
6
|
+
const getUserAgent = () => 'test-agent'
|
|
7
|
+
const YDC_API_KEY = 'test-key'
|
|
8
|
+
|
|
9
|
+
test('builds basic search request', () => {
|
|
10
|
+
const request = buildSearchRequest({
|
|
11
|
+
searchQuery: { query: 'AI' },
|
|
12
|
+
YDC_API_KEY,
|
|
13
|
+
getUserAgent,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
expect(request.url).toBe(SEARCH_API_URL)
|
|
17
|
+
expect(request.method).toBe('GET')
|
|
18
|
+
expect(request.headers['X-API-Key']).toBe('test-key')
|
|
19
|
+
expect(request.headers['User-Agent']).toBe('test-agent')
|
|
20
|
+
expect(request.queryParams?.query).toBe('AI')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('passes query with site: operator directly', () => {
|
|
24
|
+
const request = buildSearchRequest({
|
|
25
|
+
searchQuery: { query: 'AI site:you.com' },
|
|
26
|
+
YDC_API_KEY,
|
|
27
|
+
getUserAgent,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(request.queryParams?.query).toBe('AI site:you.com')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('passes query with filetype: operator directly', () => {
|
|
34
|
+
const request = buildSearchRequest({
|
|
35
|
+
searchQuery: { query: 'tutorial filetype:pdf' },
|
|
36
|
+
YDC_API_KEY,
|
|
37
|
+
getUserAgent,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(request.queryParams?.query).toBe('tutorial filetype:pdf')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('passes query with lang: operator directly', () => {
|
|
44
|
+
const request = buildSearchRequest({
|
|
45
|
+
searchQuery: { query: 'search lang:en' },
|
|
46
|
+
YDC_API_KEY,
|
|
47
|
+
getUserAgent,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(request.queryParams?.query).toBe('search lang:en')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('passes query with +term inclusion operator directly', () => {
|
|
54
|
+
const request = buildSearchRequest({
|
|
55
|
+
searchQuery: { query: 'search +machine +learning' },
|
|
56
|
+
YDC_API_KEY,
|
|
57
|
+
getUserAgent,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(request.queryParams?.query).toBe('search +machine +learning')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('passes query with -term exclusion operator directly', () => {
|
|
64
|
+
const request = buildSearchRequest({
|
|
65
|
+
searchQuery: { query: 'python -django -flask' },
|
|
66
|
+
YDC_API_KEY,
|
|
67
|
+
getUserAgent,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(request.queryParams?.query).toBe('python -django -flask')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('passes query with boolean operators directly', () => {
|
|
74
|
+
const request = buildSearchRequest({
|
|
75
|
+
searchQuery: { query: '(Python OR JavaScript) AND tutorial -deprecated' },
|
|
76
|
+
YDC_API_KEY,
|
|
77
|
+
getUserAgent,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(request.queryParams?.query).toBe('(Python OR JavaScript) AND tutorial -deprecated')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('includes advanced search parameters', () => {
|
|
84
|
+
const request = buildSearchRequest({
|
|
85
|
+
searchQuery: {
|
|
86
|
+
query: 'AI',
|
|
87
|
+
count: 10,
|
|
88
|
+
freshness: 'week',
|
|
89
|
+
offset: 5,
|
|
90
|
+
country: 'US',
|
|
91
|
+
safesearch: 'moderate',
|
|
92
|
+
livecrawl: 'web',
|
|
93
|
+
livecrawl_formats: 'markdown',
|
|
94
|
+
},
|
|
95
|
+
YDC_API_KEY,
|
|
96
|
+
getUserAgent,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(request.queryParams?.query).toBe('AI')
|
|
100
|
+
expect(request.queryParams?.count).toBe('10')
|
|
101
|
+
expect(request.queryParams?.freshness).toBe('week')
|
|
102
|
+
expect(request.queryParams?.offset).toBe('5')
|
|
103
|
+
expect(request.queryParams?.country).toBe('US')
|
|
104
|
+
expect(request.queryParams?.safesearch).toBe('moderate')
|
|
105
|
+
expect(request.queryParams?.livecrawl).toBe('web')
|
|
106
|
+
expect(request.queryParams?.livecrawl_formats).toBe('markdown')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('combines multiple operators in query string', () => {
|
|
110
|
+
const request = buildSearchRequest({
|
|
111
|
+
searchQuery: {
|
|
112
|
+
query: 'machine learning best practices (Python OR PyTorch) -TensorFlow filetype:pdf',
|
|
113
|
+
},
|
|
114
|
+
YDC_API_KEY,
|
|
115
|
+
getUserAgent,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(request.queryParams?.query).toBe(
|
|
119
|
+
'machine learning best practices (Python OR PyTorch) -TensorFlow filetype:pdf',
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { LanguageSchema, SearchQuerySchema } from '../search.schemas.ts'
|
|
3
|
+
|
|
4
|
+
describe('SearchQuerySchema OpenAPI validation', () => {
|
|
5
|
+
test('accepts valid query parameters', () => {
|
|
6
|
+
const validQueries = [
|
|
7
|
+
{ query: 'AI' },
|
|
8
|
+
{ query: 'test', count: 10, freshness: 'week' },
|
|
9
|
+
{ query: 'search', country: 'US', safesearch: 'moderate' },
|
|
10
|
+
{ query: 'livecrawl test', livecrawl: 'web', livecrawl_formats: 'markdown' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
for (const validQuery of validQueries) {
|
|
14
|
+
expect(() => SearchQuerySchema.parse(validQuery)).not.toThrow()
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('rejects invalid query parameters', () => {
|
|
19
|
+
const invalidQueries = [
|
|
20
|
+
{}, // Missing query
|
|
21
|
+
{ query: '' }, // Empty query
|
|
22
|
+
{ query: 'test', count: 0 }, // Count too low
|
|
23
|
+
{ query: 'test', count: 101 }, // Count too high
|
|
24
|
+
{ query: 'test', offset: -1 }, // Negative offset
|
|
25
|
+
{ query: 'test', offset: 10 }, // Offset too high
|
|
26
|
+
{ query: 'test', safesearch: 'invalid' }, // Invalid safesearch
|
|
27
|
+
{ query: 'test', livecrawl: 'invalid' }, // Invalid livecrawl
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
for (const invalidQuery of invalidQueries) {
|
|
31
|
+
expect(() => SearchQuerySchema.parse(invalidQuery)).toThrow()
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('LanguageSchema includes all 51 BCP 47 codes', () => {
|
|
36
|
+
// Test a sample of language codes from the OpenAPI spec
|
|
37
|
+
const languageCodes = [
|
|
38
|
+
'AR',
|
|
39
|
+
'EU',
|
|
40
|
+
'BN',
|
|
41
|
+
'BG',
|
|
42
|
+
'CA',
|
|
43
|
+
'ZH-HANS',
|
|
44
|
+
'ZH-HANT',
|
|
45
|
+
'HR',
|
|
46
|
+
'CS',
|
|
47
|
+
'DA',
|
|
48
|
+
'NL',
|
|
49
|
+
'EN',
|
|
50
|
+
'EN-GB',
|
|
51
|
+
'ET',
|
|
52
|
+
'FI',
|
|
53
|
+
'FR',
|
|
54
|
+
'GL',
|
|
55
|
+
'DE',
|
|
56
|
+
'EL',
|
|
57
|
+
'GU',
|
|
58
|
+
'HE',
|
|
59
|
+
'HI',
|
|
60
|
+
'HU',
|
|
61
|
+
'IS',
|
|
62
|
+
'IT',
|
|
63
|
+
'JP',
|
|
64
|
+
'KN',
|
|
65
|
+
'KO',
|
|
66
|
+
'LV',
|
|
67
|
+
'LT',
|
|
68
|
+
'MS',
|
|
69
|
+
'ML',
|
|
70
|
+
'MR',
|
|
71
|
+
'NB',
|
|
72
|
+
'PL',
|
|
73
|
+
'PT-BR',
|
|
74
|
+
'PT-PT',
|
|
75
|
+
'PA',
|
|
76
|
+
'RO',
|
|
77
|
+
'RU',
|
|
78
|
+
'SR',
|
|
79
|
+
'SK',
|
|
80
|
+
'SL',
|
|
81
|
+
'ES',
|
|
82
|
+
'SV',
|
|
83
|
+
'TA',
|
|
84
|
+
'TE',
|
|
85
|
+
'TH',
|
|
86
|
+
'TR',
|
|
87
|
+
'UK',
|
|
88
|
+
'VI',
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
for (const code of languageCodes) {
|
|
92
|
+
expect(() => LanguageSchema.parse(code)).not.toThrow()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Test that the schema has exactly 51 values
|
|
96
|
+
expect(LanguageSchema.options.length).toBe(51)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('rejects invalid language codes', () => {
|
|
100
|
+
const invalidCodes = ['XX', 'INVALID', 'en', 'zh', '123']
|
|
101
|
+
|
|
102
|
+
for (const code of invalidCodes) {
|
|
103
|
+
expect(() => LanguageSchema.parse(code)).toThrow()
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('accepts all country codes from OpenAPI spec', () => {
|
|
108
|
+
const countryCodes = [
|
|
109
|
+
'AR',
|
|
110
|
+
'AU',
|
|
111
|
+
'AT',
|
|
112
|
+
'BE',
|
|
113
|
+
'BR',
|
|
114
|
+
'CA',
|
|
115
|
+
'CL',
|
|
116
|
+
'DK',
|
|
117
|
+
'FI',
|
|
118
|
+
'FR',
|
|
119
|
+
'DE',
|
|
120
|
+
'HK',
|
|
121
|
+
'IN',
|
|
122
|
+
'ID',
|
|
123
|
+
'IT',
|
|
124
|
+
'JP',
|
|
125
|
+
'KR',
|
|
126
|
+
'MY',
|
|
127
|
+
'MX',
|
|
128
|
+
'NL',
|
|
129
|
+
'NZ',
|
|
130
|
+
'NO',
|
|
131
|
+
'CN',
|
|
132
|
+
'PL',
|
|
133
|
+
'PT',
|
|
134
|
+
'PT-BR',
|
|
135
|
+
'PH',
|
|
136
|
+
'RU',
|
|
137
|
+
'SA',
|
|
138
|
+
'ZA',
|
|
139
|
+
'ES',
|
|
140
|
+
'SE',
|
|
141
|
+
'CH',
|
|
142
|
+
'TW',
|
|
143
|
+
'TR',
|
|
144
|
+
'GB',
|
|
145
|
+
'US',
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
for (const code of countryCodes) {
|
|
149
|
+
expect(() => SearchQuerySchema.parse({ query: 'test', country: code })).not.toThrow()
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -15,9 +15,7 @@ describe('fetchSearchResults', () => {
|
|
|
15
15
|
expect(result).toHaveProperty('results')
|
|
16
16
|
expect(result).toHaveProperty('metadata')
|
|
17
17
|
expect(result.results).toHaveProperty('web')
|
|
18
|
-
// expect(result.results).toHaveProperty('news');
|
|
19
18
|
expect(Array.isArray(result.results.web)).toBe(true)
|
|
20
|
-
// expect(Array.isArray(result.results.news)).toBe(true);
|
|
21
19
|
|
|
22
20
|
// Assert required metadata fields
|
|
23
21
|
expect(typeof result.metadata?.query).toBe('string')
|
|
@@ -57,7 +55,6 @@ describe('fetchSearchResults', () => {
|
|
|
57
55
|
})
|
|
58
56
|
|
|
59
57
|
// Test that web results have required properties
|
|
60
|
-
// biome-ignore lint/style/noNonNullAssertion: Test
|
|
61
58
|
const webResult = result.results.web![0]
|
|
62
59
|
|
|
63
60
|
expect(webResult).toHaveProperty('url')
|
|
@@ -65,13 +62,6 @@ describe('fetchSearchResults', () => {
|
|
|
65
62
|
expect(webResult).toHaveProperty('description')
|
|
66
63
|
expect(webResult).toHaveProperty('snippets')
|
|
67
64
|
expect(Array.isArray(webResult?.snippets)).toBe(true)
|
|
68
|
-
|
|
69
|
-
// Test that news results have required properties
|
|
70
|
-
// const newsResult = result.results.news![0];
|
|
71
|
-
// expect(newsResult).toHaveProperty('url');
|
|
72
|
-
// expect(newsResult).toHaveProperty('title');
|
|
73
|
-
// expect(newsResult).toHaveProperty('description');
|
|
74
|
-
// expect(newsResult).toHaveProperty('page_age');
|
|
75
65
|
},
|
|
76
66
|
{ retry: 2 },
|
|
77
67
|
)
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const SEARCH_API_URL = 'https://ydc-index.io/v1/search'
|
|
9
|
-
export const
|
|
9
|
+
export const DEEP_SEARCH_API_URL = 'https://api.you.com/v1/deep_search'
|
|
10
10
|
export const CONTENTS_API_URL = 'https://ydc-index.io/v1/contents'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Checks if a response object contains an error field and throws if found
|
|
3
3
|
* Handles API responses that return 200 status but contain error messages
|
|
4
|
-
* Used by
|
|
4
|
+
* Used by search, deep-search, and contents utilities
|
|
5
5
|
*/
|
|
6
6
|
export const checkResponseForErrors = (responseData: unknown) => {
|
|
7
7
|
if (typeof responseData === 'object' && responseData !== null && 'error' in responseData) {
|