@youdotcom-oss/api 0.1.0 → 0.2.0

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.
@@ -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 EXPRESS_API_URL = 'https://api.you.com/v1/agents/runs'
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 both search and express agent utilities
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) {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Shared command infrastructure for CLI commands
3
+ * Handles flag parsing, validation, and execution
4
+ *
5
+ * @internal
6
+ */
7
+
8
+ import { parseArgs } from 'node:util'
9
+ import * as z from 'zod'
10
+ import type { GetUserAgent } from './api.types.ts'
11
+ import type { DryRunResult } from './dry-run-utils.ts'
12
+ import { useGetUserAgent } from './use-get-user-agents.ts'
13
+
14
+ /**
15
+ * Configuration for a command
16
+ *
17
+ * @typeParam TInput - Zod-inferred input type
18
+ * @typeParam TOutput - Command output type
19
+ */
20
+ export type CommandConfig<TInput, TOutput> = {
21
+ schema: z.ZodType<TInput>
22
+ handler: (params: { input: TInput; YDC_API_KEY: string; getUserAgent: GetUserAgent }) => Promise<TOutput>
23
+ dryRunHandler?: (params: { input: TInput; YDC_API_KEY: string; getUserAgent: GetUserAgent }) => DryRunResult
24
+ }
25
+
26
+ /**
27
+ * Run a command with standardized flag parsing and validation
28
+ * Handles --schema, --json, --api-key, --client, and --dry-run flags
29
+ *
30
+ * @param args - Command line arguments
31
+ * @param config - Command configuration with schema and handler
32
+ *
33
+ * @internal
34
+ */
35
+ export const runCommand = async <TInput, TOutput>(args: string[], config: CommandConfig<TInput, TOutput>) => {
36
+ // Handle --schema flag
37
+ if (args.includes('--schema')) {
38
+ console.log(JSON.stringify(z.toJSONSchema(config.schema)))
39
+ process.exit(0)
40
+ }
41
+
42
+ // Parse flags with Node's built-in parseArgs
43
+ const { values } = parseArgs({
44
+ args,
45
+ options: {
46
+ json: { type: 'string' },
47
+ 'api-key': { type: 'string' },
48
+ client: { type: 'string' },
49
+ 'dry-run': { type: 'boolean' },
50
+ },
51
+ })
52
+
53
+ // --json is required
54
+ if (!values.json) {
55
+ throw new Error('--json flag is required')
56
+ }
57
+
58
+ // Parse JSON input
59
+ const input = JSON.parse(values.json)
60
+ const apiKey = values['api-key']
61
+ const client = values.client || process.env.YDC_CLIENT
62
+
63
+ // Get API key from options or environment
64
+ const YDC_API_KEY = apiKey || process.env.YDC_API_KEY
65
+ if (!YDC_API_KEY) {
66
+ throw new Error('YDC_API_KEY environment variable is required')
67
+ }
68
+
69
+ // Validate with schema
70
+ const validatedInput = config.schema.parse(input)
71
+
72
+ // Create getUserAgent function
73
+ const getUserAgent = useGetUserAgent(client)
74
+
75
+ // Handle --dry-run flag
76
+ if (values['dry-run'] && config.dryRunHandler) {
77
+ const dryRunResult = config.dryRunHandler({
78
+ input: validatedInput,
79
+ YDC_API_KEY,
80
+ getUserAgent,
81
+ })
82
+ console.log(JSON.stringify(dryRunResult))
83
+ return
84
+ }
85
+
86
+ // Execute handler
87
+ const response = await config.handler({
88
+ input: validatedInput,
89
+ YDC_API_KEY,
90
+ getUserAgent,
91
+ })
92
+
93
+ // Output response to stdout (success)
94
+ console.log(JSON.stringify(response))
95
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Dry-run utilities for testing request construction without API calls
3
+ * These functions build request details (URL, headers, body) that can be inspected
4
+ * without making actual API calls.
5
+ *
6
+ * @public
7
+ */
8
+
9
+ import type { ContentsQuery } from '../contents/contents.schemas.ts'
10
+ import type { DeepSearchQuery } from '../deep-search/deep-search.schemas.ts'
11
+ import type { SearchQuery } from '../search/search.schemas.ts'
12
+ import { CONTENTS_API_URL, DEEP_SEARCH_API_URL, SEARCH_API_URL } from './api.constants.ts'
13
+ import type { GetUserAgent } from './api.types.ts'
14
+
15
+ /**
16
+ * Result structure for dry-run request inspection
17
+ *
18
+ * @public
19
+ */
20
+ export type DryRunResult = {
21
+ url: string
22
+ method: 'GET' | 'POST'
23
+ headers: Record<string, string>
24
+ body?: string
25
+ queryParams?: Record<string, string>
26
+ }
27
+
28
+ /**
29
+ * Build search request details without making API call
30
+ * Useful for testing and debugging query construction
31
+ *
32
+ * @param params - Search query parameters
33
+ * @returns Request details including URL, headers, and query params
34
+ *
35
+ * @public
36
+ */
37
+ export const buildSearchRequest = ({
38
+ searchQuery,
39
+ YDC_API_KEY,
40
+ getUserAgent,
41
+ }: {
42
+ searchQuery: SearchQuery
43
+ YDC_API_KEY: string
44
+ getUserAgent: GetUserAgent
45
+ }): DryRunResult => {
46
+ // Convert all search query params to query string parameters
47
+ const queryParams: Record<string, string> = {}
48
+
49
+ for (const [name, value] of Object.entries(searchQuery)) {
50
+ if (value !== undefined && value !== null) {
51
+ queryParams[name] = `${value}`
52
+ }
53
+ }
54
+
55
+ return {
56
+ url: SEARCH_API_URL,
57
+ method: 'GET',
58
+ headers: {
59
+ 'X-API-Key': YDC_API_KEY,
60
+ 'User-Agent': getUserAgent(),
61
+ },
62
+ queryParams,
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Build contents request details without making API call
68
+ * Useful for testing and debugging POST body construction
69
+ *
70
+ * @param params - Contents query parameters
71
+ * @returns Request details including URL, headers, and POST body
72
+ *
73
+ * @public
74
+ */
75
+ export const buildContentsRequest = ({
76
+ contentsQuery: { urls, formats, format, crawl_timeout },
77
+ YDC_API_KEY,
78
+ getUserAgent,
79
+ }: {
80
+ contentsQuery: ContentsQuery
81
+ YDC_API_KEY: string
82
+ getUserAgent: GetUserAgent
83
+ }): DryRunResult => {
84
+ // Handle backward compatibility: prefer formats array, fallback to format string, default to ['markdown']
85
+ const requestFormats = formats || (format ? [format] : ['markdown'])
86
+
87
+ // Build request body
88
+ const requestBody: {
89
+ urls: string[]
90
+ formats: string[]
91
+ crawl_timeout?: number
92
+ } = {
93
+ urls,
94
+ formats: requestFormats,
95
+ }
96
+
97
+ if (crawl_timeout !== undefined) {
98
+ requestBody.crawl_timeout = crawl_timeout
99
+ }
100
+
101
+ return {
102
+ url: CONTENTS_API_URL,
103
+ method: 'POST',
104
+ headers: {
105
+ 'X-API-Key': YDC_API_KEY,
106
+ 'Content-Type': 'application/json',
107
+ 'User-Agent': getUserAgent(),
108
+ },
109
+ body: JSON.stringify(requestBody),
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Build deep-search request details without making API call
115
+ * Useful for testing and debugging POST body construction
116
+ *
117
+ * @param params - Deep-search query parameters
118
+ * @returns Request details including URL, headers, and POST body
119
+ *
120
+ * @public
121
+ */
122
+ export const buildDeepSearchRequest = ({
123
+ deepSearchQuery,
124
+ YDC_API_KEY,
125
+ getUserAgent,
126
+ }: {
127
+ deepSearchQuery: DeepSearchQuery
128
+ YDC_API_KEY: string
129
+ getUserAgent: GetUserAgent
130
+ }): DryRunResult => {
131
+ return {
132
+ url: DEEP_SEARCH_API_URL,
133
+ method: 'POST',
134
+ headers: {
135
+ 'X-API-Key': YDC_API_KEY,
136
+ 'Content-Type': 'application/json',
137
+ 'User-Agent': getUserAgent(),
138
+ },
139
+ body: JSON.stringify(deepSearchQuery),
140
+ }
141
+ }