diff --git a/mod.ts b/mod.ts index 3eaa3f4..5e101a3 100644 --- a/mod.ts +++ b/mod.ts @@ -12,7 +12,15 @@ export type { BaseResponse, EngineParameters, GetBySearchIdParameters, + GoogleSearchParameters, + GoogleSearchResponse, + KnowledgeGraph, LocationsApiParameters, + OrganicResult, + RelatedQuestion, + SearchInformation, + SearchMetadata, + SearchParameters, } from "./src/types.ts"; export { getAccount, diff --git a/src/engines/google.ts b/src/engines/google.ts new file mode 100644 index 0000000..cca76c9 --- /dev/null +++ b/src/engines/google.ts @@ -0,0 +1,133 @@ +/** Parameters for the Google Search engine. */ +export interface GoogleSearchParameters { + engine: "google"; + q: string; + api_key?: string; + timeout?: number; + + // Geographic Location + location?: string; + uule?: string; + lat?: number; + lon?: number; + radius?: number; + + // Localization + google_domain?: string; + gl?: string; + hl?: string; + cr?: string; + lr?: string; + + // Search Type + tbm?: "isch" | "lcl" | "vid" | "nws" | "shop" | "pts"; + + // Pagination + start?: number; + num?: number; + + // Advanced Filters + tbs?: string; + safe?: "active" | "off"; + nfpr?: 0 | 1; + filter?: 0 | 1; + + // Advanced Google Parameters + ludocid?: string; + lsig?: string; + kgmid?: string; + si?: string; + + // SerpApi Parameters + device?: "desktop" | "tablet" | "mobile"; + no_cache?: boolean; + async?: boolean; + + // deno-lint-ignore no-explicit-any + [key: string]: any; +} + +/** Metadata returned with every SerpApi response. */ +export interface SearchMetadata { + id: string; + status: string; + json_endpoint: string; + created_at: string; + processed_at: string; + google_url: string; + raw_html_file: string; + total_time_taken: number; +} + +/** Echoed search parameters in the response. */ +export interface SearchParameters { + engine: string; + q: string; + google_domain?: string; + hl?: string; + gl?: string; + device?: string; + location_requested?: string; + location_used?: string; + [key: string]: unknown; +} + +/** Search result info. */ +export interface SearchInformation { + query_displayed: string; + total_results: number; + time_taken_displayed: number; + organic_results_state: string; + page_number?: number; +} + +/** A single organic search result. */ +export interface OrganicResult { + position: number; + title: string; + link: string; + redirect_link?: string; + displayed_link: string; + snippet: string; + snippet_highlighted_words?: string[]; + date?: string; + sitelinks?: { + inline?: { title: string; link: string }[]; + expanded?: { title: string; link: string; snippet: string }[]; + }; + rich_snippet?: Record; + source?: string; +} + +/** Knowledge graph result. */ +export interface KnowledgeGraph { + title?: string; + type?: string; + description?: string; + source?: { name: string; link: string }; + [key: string]: unknown; +} + +/** "People also ask" question. */ +export interface RelatedQuestion { + question: string; + snippet?: string; + title?: string; + link?: string; +} + +/** Google Search JSON response. */ +export interface GoogleSearchResponse { + search_metadata: SearchMetadata; + search_parameters: SearchParameters; + search_information?: SearchInformation; + organic_results?: OrganicResult[]; + knowledge_graph?: KnowledgeGraph; + related_questions?: RelatedQuestion[]; + pagination?: { + current: number; + next?: string; + other_pages?: Record; + }; + [key: string]: unknown; +} diff --git a/src/serpapi.ts b/src/serpapi.ts index 9422ea7..cb7c659 100644 --- a/src/serpapi.ts +++ b/src/serpapi.ts @@ -4,6 +4,8 @@ import { BaseResponse, EngineParameters, GetBySearchIdParameters, + GoogleSearchParameters, + GoogleSearchResponse, LocationsApiParameters, } from "./types.ts"; import { _internals } from "./utils.ts"; @@ -14,6 +16,14 @@ const LOCATIONS_PATH = "/locations.json"; const SEARCH_PATH = "/search"; const SEARCH_ARCHIVE_PATH = `/searches`; +/** + * Get typed JSON response for Google Search. + */ +export function getJson( + parameters: GoogleSearchParameters, + callback?: (json: GoogleSearchResponse) => void, +): Promise; + /** * Get JSON response based on search parameters. * @@ -52,13 +62,15 @@ export function getJson( export function getJson( ...args: - | [parameters: EngineParameters, callback?: (json: BaseResponse) => void] + // deno-lint-ignore no-explicit-any + | [parameters: EngineParameters, callback?: (json: any) => void] | [ engine: string, parameters: EngineParameters, - callback?: (json: BaseResponse) => void, + // deno-lint-ignore no-explicit-any + callback?: (json: any) => void, ] -): Promise { +): Promise { if (typeof args[0] === "string" && typeof args[1] === "object") { const [engine, parameters, callback] = args; const newParameters = { ...parameters, engine } as EngineParameters; diff --git a/src/types.ts b/src/types.ts index c83e861..a5fd037 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,17 @@ export type EngineParameters = Record; // deno-lint-ignore no-explicit-any export type BaseResponse = Record; +export type { + GoogleSearchParameters, + GoogleSearchResponse, + OrganicResult, + KnowledgeGraph, + RelatedQuestion, + SearchInformation, + SearchMetadata, + SearchParameters, +} from "./engines/google.ts"; + export type GetBySearchIdParameters = { api_key?: string; timeout?: number; diff --git a/tests/types_test.ts b/tests/types_test.ts new file mode 100644 index 0000000..0fc05d7 --- /dev/null +++ b/tests/types_test.ts @@ -0,0 +1,92 @@ +/** + * Type-level tests for engine-specific types. + * These verify compile-time behavior — if this file fails to type-check, + * the types are broken. + */ +import type { + EngineParameters, + GoogleSearchParameters, + GoogleSearchResponse, + OrganicResult, +} from "../src/types.ts"; +import { getJson } from "../src/serpapi.ts"; +import { + describe, + it, +} from "https://deno.land/std@0.170.0/testing/bdd.ts"; +import { + assertEquals, +} from "https://deno.land/std@0.170.0/testing/asserts.ts"; + +describe("GoogleSearchParameters", () => { + it("accepts valid Google Search params", () => { + const params: GoogleSearchParameters = { + engine: "google", + q: "coffee", + location: "Austin, Texas", + gl: "us", + hl: "en", + num: 10, + start: 0, + safe: "active", + device: "desktop", + }; + assertEquals(params.engine, "google"); + assertEquals(params.q, "coffee"); + }); + + it("allows tbm search type values", () => { + const params: GoogleSearchParameters = { + engine: "google", + q: "coffee", + tbm: "nws", + }; + assertEquals(params.tbm, "nws"); + }); +}); + +describe("GoogleSearchResponse", () => { + it("has correct shape", () => { + const response: GoogleSearchResponse = { + search_metadata: { + id: "123", + status: "Success", + json_endpoint: "https://serpapi.com/searches/123.json", + created_at: "2025-01-01", + processed_at: "2025-01-01", + google_url: "https://www.google.com/search?q=coffee", + raw_html_file: "https://serpapi.com/searches/123.html", + total_time_taken: 1.5, + }, + search_parameters: { + engine: "google", + q: "coffee", + }, + organic_results: [ + { + position: 1, + title: "Coffee - Wikipedia", + link: "https://en.wikipedia.org/wiki/Coffee", + displayed_link: "en.wikipedia.org", + snippet: "Coffee is a brewed drink...", + }, + ], + }; + assertEquals(response.search_metadata.status, "Success"); + + const firstResult: OrganicResult = response.organic_results![0]; + assertEquals(firstResult.position, 1); + assertEquals(firstResult.title, "Coffee - Wikipedia"); + }); +}); + +describe("backwards compatibility", () => { + it("generic EngineParameters still works", () => { + const params: EngineParameters = { + engine: "bing", + q: "anything", + custom_field: 123, + }; + assertEquals(params.engine, "bing"); + }); +});