From e68a2d38549b8ed78b58e105ef4ed68d9c031a8c Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Sat, 21 Feb 2026 22:04:38 +0100 Subject: [PATCH 1/7] chore: add IntelliJ files to gitignore # Conflicts: # .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 096852c..e7771b0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ coverage !.vscode/extensions.json !.vscode/settings.json +# IntelliJ IDEA +.idea/ + # Docusaurus website/node_modules website/.docusaurus From 8d326ec70cd96a501323f578606b05d14cdffe81 Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Sat, 21 Feb 2026 22:31:18 +0100 Subject: [PATCH 2/7] feat: add graphql endpoint to search games on the ps store --- src/graphql/getSearchResults.test.ts | 201 ++++++++++++++++++++ src/graphql/getSearchResults.ts | 65 +++++++ src/graphql/index.ts | 1 + src/graphql/operationHashes.ts | 3 + src/models/index.ts | 1 + src/models/search-results-response.model.ts | 65 +++++++ src/utils/call.ts | 17 +- 7 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 src/graphql/getSearchResults.test.ts create mode 100644 src/graphql/getSearchResults.ts create mode 100644 src/models/search-results-response.model.ts diff --git a/src/graphql/getSearchResults.test.ts b/src/graphql/getSearchResults.test.ts new file mode 100644 index 0000000..3e7a4e7 --- /dev/null +++ b/src/graphql/getSearchResults.test.ts @@ -0,0 +1,201 @@ +import nock from "nock"; + +import type { SearchResultsResponse } from "../models"; +import { getSearchResults } from "./getSearchResults"; +import { GRAPHQL_BASE_URL } from "./GRAPHQL_BASE_URL"; + +describe("Function: getSearchResults", () => { + afterEach(() => { + nock.cleanAll(); + }); + + it("is defined #sanity", () => { + // ASSERT + expect(getSearchResults).toBeDefined(); + }); + + it("retrieves search results for a given search term", async () => { + // ARRANGE + const mockResponse: SearchResultsResponse = { + data: { + universalSearch: { + __typename: "UniversalSearchResponse", + next: "CDAaVQolYjg5ODE2ODk0Y2ZiNDU2ZGIzNTU5MjIwOWM5YTdjODQtNDAxNxIsc2VhcmNoLXJlbGV2YW5jeS1jb25jZXB0LWdhbWUtYWxsLXRvcGstYXN0cmEiHnNlYXJjaC5ub19leHBlcmltZW50Lm5vbi4wLm5vbioDNzUx", + pageInfo: { + __typename: "PageInfo", + isLast: false, + offset: 0, + size: 2, + totalCount: 87 + }, + results: [ + { + __typename: "Product", + id: "EP1018-CUSA00135_00-BAKPREORDER00001", + localizedStoreDisplayClassification: "Volledige game", + media: [ + { + __typename: "Media", + role: "MASTER", + type: "IMAGE", + url: "https://image.api.playstation.com/cdn/EP1018/CUSA00135_00/gIBLibuNu1I91g9FYzkqBJFLMd1X9OaD.png" + } + ], + name: "Batman: Arkham Knight", + npTitleId: "CUSA00135_00", + personalizedMeta: { + __typename: "PersonalizedMeta", + hasMediaOverrides: false, + media: [ + { + __typename: "Media", + role: "MASTER", + type: "IMAGE", + url: "https://image.api.playstation.com/cdn/EP1018/CUSA00135_00/gIBLibuNu1I91g9FYzkqBJFLMd1X9OaD.png" + } + ] + }, + platforms: ["PS4"], + price: { + __typename: "SkuPrice", + skuId: "EP1018-CUSA00135_00-BAKPREORDER00001-E005", + basePrice: "€19.99", + discountText: null, + discountedPrice: null, + includesBundleOffer: null, + isExclusive: false, + isFree: false, + isTiedToSubscription: null, + serviceBranding: null, + upsellServiceBranding: [], + upsellText: null + }, + skus: [ + { + __typename: "Sku", + type: "STANDARD" + } + ], + storeDisplayClassification: "FULL_GAME" + }, + { + __typename: "Product", + id: "EP1018-CUSA00135_00-DLCTWOFACESTORY0", + localizedStoreDisplayClassification: "Level", + media: [ + { + __typename: "Media", + role: "MASTER", + type: "IMAGE", + url: "https://image.api.playstation.com/cdn/EP1018/CUSA00135_00/U2Yf57C14ovXI3zl3uXWoHSli3SywqEr.png" + } + ], + name: "Batman™: Arkham Knight Kop of munt", + npTitleId: "CUSA00135_00", + personalizedMeta: { + __typename: "PersonalizedMeta", + hasMediaOverrides: false, + media: [ + { + __typename: "Media", + role: "MASTER", + type: "IMAGE", + url: "https://image.api.playstation.com/cdn/EP1018/CUSA00135_00/U2Yf57C14ovXI3zl3uXWoHSli3SywqEr.png" + } + ] + }, + platforms: ["PS4"], + price: { + __typename: "SkuPrice", + skuId: "EP1018-CUSA00135_00-DLCTWOFACESTORY0-E001", + basePrice: "€1.99", + discountText: null, + discountedPrice: null, + includesBundleOffer: null, + isExclusive: false, + isFree: false, + isTiedToSubscription: null, + serviceBranding: null, + upsellServiceBranding: [], + upsellText: null + }, + skus: [ + { + __typename: "Sku", + type: "STANDARD" + } + ], + storeDisplayClassification: "LEVEL" + } + ] + } + } + }; + + const baseUrlObj = new URL(GRAPHQL_BASE_URL); + const baseUrl = `${baseUrlObj.protocol}//${baseUrlObj.host}`; + const basePath = baseUrlObj.pathname; + + // ... we need to use a nock matcher to verify the query parameters ... + const expectedVariables = JSON.stringify({ + countryCode: "NL", + languageCode: "nl", + pageSize: 2, + pageOffset: 0, + searchTerm: "batman" + }); + const expectedExtensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: + "6ef5e809c35a056a1150fdcf513d9c505484dd1a946b6208888435c3182f105a" + } + }); + + const mockScope = nock(baseUrl) + .get(basePath) + .query((params) => { + expect(params.operationName).toEqual("getSearchResults"); + expect(params.variables).toEqual(expectedVariables); + expect(params.extensions).toEqual(expectedExtensions); + return true; + }) + .reply(200, mockResponse); + + // ACT + const response = await getSearchResults("batman", { + countryCode: "NL", + languageCode: "nl", + pageSize: 2 + }); + + // ASSERT + expect(response).toEqual(mockResponse); + expect(mockScope.isDone()).toBeTruthy(); + }); + + it("throws an error if we receive a malformed response", async () => { + // ARRANGE + const mockResponse = { + // This response occurs if the query/hash is not what the server expected. + message: + "Query 6ef5e809c35a056a1150fdcf513d9c505484dd1a946b6208888435c3182f105a not whitelisted" + }; + + const baseUrlObj = new URL(GRAPHQL_BASE_URL); + const baseUrl = `${baseUrlObj.protocol}//${baseUrlObj.host}`; + const basePath = baseUrlObj.pathname; + + nock(baseUrl).get(basePath).query(true).reply(200, mockResponse); + + // ASSERT + await expect( + getSearchResults("test", { + countryCode: "US", + languageCode: "en" + }) + ).rejects.toThrow( + "Query 6ef5e809c35a056a1150fdcf513d9c505484dd1a946b6208888435c3182f105a not whitelisted" + ); + }); +}); diff --git a/src/graphql/getSearchResults.ts b/src/graphql/getSearchResults.ts new file mode 100644 index 0000000..9619910 --- /dev/null +++ b/src/graphql/getSearchResults.ts @@ -0,0 +1,65 @@ +import type { SearchResultsResponse } from "../models"; +import { call } from "../utils/call"; +import { GRAPHQL_BASE_URL } from "./GRAPHQL_BASE_URL"; +import { getSearchResultsHash } from "./operationHashes"; + +type GetSearchResultsOptions = { + countryCode: string; + languageCode: string; + pageSize?: number; + pageOffset?: number; +}; + +/** + * A call to this function will retrieve search results from the PlayStation Store + * based on the provided search term and pagination. + * + * NOTE: This endpoint should be called WITHOUT authorization to receive price information. + * When called with authorization, price data may be filtered or unavailable. + * + * @param searchTerm The search term to query for. + * @param options An object containing search options including countryCode and languageCode. + */ +export const getSearchResults = async ( + searchTerm: string, + options: GetSearchResultsOptions +): Promise => { + const { countryCode, languageCode, pageSize = 24, pageOffset = 0 } = options; + + const url = new URL(GRAPHQL_BASE_URL); + + url.searchParams.set("operationName", "getSearchResults"); + + const variables: Record = { + countryCode, + languageCode, + pageSize, + pageOffset, + searchTerm + }; + + url.searchParams.set("variables", JSON.stringify(variables)); + url.searchParams.set( + "extensions", + JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: getSearchResultsHash + } + }) + ); + + const response = await call({ + url: url.toString(), + headers: { + "Accept-Language": `${languageCode}-${countryCode}` + } + }); + + // The GraphQL queries can return non-truthy values. + if (!response.data || !response.data.universalSearch) { + throw new Error(JSON.stringify(response)); + } + + return response; +}; diff --git a/src/graphql/index.ts b/src/graphql/index.ts index a7da725..10d23bb 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -1,2 +1,3 @@ export * from "./getPurchasedGames"; export * from "./getRecentlyPlayedGames"; +export * from "./getSearchResults"; diff --git a/src/graphql/operationHashes.ts b/src/graphql/operationHashes.ts index 5567e2f..d092add 100644 --- a/src/graphql/operationHashes.ts +++ b/src/graphql/operationHashes.ts @@ -22,3 +22,6 @@ export const getUserGameListHash = export const getPurchasedGameListHash = "827a423f6a8ddca4107ac01395af2ec0eafd8396fc7fa204aaf9b7ed2eefa168"; + +export const getSearchResultsHash = + "6ef5e809c35a056a1150fdcf513d9c505484dd1a946b6208888435c3182f105a"; diff --git a/src/models/index.ts b/src/models/index.ts index 59eb182..8361a66 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -12,6 +12,7 @@ export * from "./profile-from-user-name-response.model"; export * from "./purchased-games-response.model"; export * from "./rarest-thin-trophy.model"; export * from "./recently-played-games-response.model"; +export * from "./search-results-response.model"; export * from "./shareable-profile-link-response.model"; export * from "./social-account-result.model"; export * from "./title-platform.model"; diff --git a/src/models/search-results-response.model.ts b/src/models/search-results-response.model.ts new file mode 100644 index 0000000..c630ce3 --- /dev/null +++ b/src/models/search-results-response.model.ts @@ -0,0 +1,65 @@ +export interface SearchResultMedia { + __typename: "Media"; + role: string; + type: "IMAGE" | "VIDEO"; + url: string; +} + +export interface SearchResultPersonalizedMeta { + __typename: "PersonalizedMeta"; + hasMediaOverrides: boolean; + media: SearchResultMedia[]; +} + +export interface SearchResultSkuPrice { + __typename: "SkuPrice"; + skuId: string; + basePrice: string | null; + discountText: string | null; + discountedPrice: string | null; + includesBundleOffer: boolean | null; + isExclusive: boolean; + isFree: boolean; + isTiedToSubscription: boolean | null; + serviceBranding: string[]; + upsellServiceBranding: string[]; + upsellText: string | null; +} + +export interface SearchResultSku { + __typename: "Sku"; + type: string; +} + +export interface SearchResultProduct { + __typename: "Product"; + id: string; + name: string; + npTitleId: string; + platforms: string[]; + localizedStoreDisplayClassification: string; + storeDisplayClassification: string; + media: SearchResultMedia[]; + personalizedMeta: SearchResultPersonalizedMeta; + price: SearchResultSkuPrice; + skus: SearchResultSku[]; +} + +export interface SearchResultPageInfo { + __typename: "PageInfo"; + isLast: boolean; + offset: number; + size: number; + totalCount: number; +} + +export interface SearchResultsResponse { + data: { + universalSearch: { + __typename: "UniversalSearchResponse"; + next: string | null; + pageInfo: SearchResultPageInfo; + results: SearchResultProduct[]; + }; + }; +} diff --git a/src/utils/call.ts b/src/utils/call.ts index 53df58a..251fb16 100644 --- a/src/utils/call.ts +++ b/src/utils/call.ts @@ -6,16 +6,21 @@ export const call = async ( method?: "GET" | "POST"; headers?: CallValidHeaders; }, - authorization: AuthorizationPayload, + authorization?: AuthorizationPayload, bodyPayload?: Record ) => { + const headers: Record = { + "Content-Type": "application/json", + ...config.headers + }; + + if (authorization?.accessToken) { + headers.Authorization = `Bearer ${authorization.accessToken}`; + } + const response = await fetch(config.url, { method: config?.method ?? "GET", - headers: { - Authorization: `Bearer ${authorization.accessToken}`, - "Content-Type": "application/json", - ...config?.headers - }, + headers, body: JSON.stringify(bodyPayload) }); From b26a7a152577bc895d5047105da327c5da7e3524 Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Sun, 22 Feb 2026 14:11:26 +0100 Subject: [PATCH 3/7] docs: add getSearchResults endpoint to the api-docs --- README.md | 1 + .../data-models/search-results-response.md | 71 +++++++++++++++++++ website/docs/api-docs/universal-search.md | 61 ++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 website/docs/api-docs/data-models/search-results-response.md diff --git a/README.md b/README.md index 44765ea..f426647 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Click the function names to open their complete docs on the docs site. ### Search - [`makeUniversalSearch()`](https://psn-api.achievements.app/api-docs/universal-search#makeuniversalsearch) - Search the PSN API. This is a good way to find a user's `accountId` from their username. +- [`getSearchResults()`](https://psn-api.achievements.app/api-docs/universal-search#getsearchresults) - Search for games, DLC, and other products on the PlayStation Store. ### Users diff --git a/website/docs/api-docs/data-models/search-results-response.md b/website/docs/api-docs/data-models/search-results-response.md new file mode 100644 index 0000000..c4bd0fc --- /dev/null +++ b/website/docs/api-docs/data-models/search-results-response.md @@ -0,0 +1,71 @@ +# SearchResultsResponse + +## SearchResultsResponse + +| Name | Type | Description | +| :------------------------------ | :---------------------------------------------- | :---------------------------------------------- | +| `data.universalSearch` | `object` | The main search results container. | +| `data.universalSearch.next` | `string \| null` | Pagination cursor for the next page of results. | +| `data.universalSearch.pageInfo` | [`SearchResultPageInfo`](#searchresultpageinfo) | Pagination information. | +| `data.universalSearch.results` | [`SearchResultProduct[]`](#searchresultproduct) | Array of product results. | + +## SearchResultPageInfo + +| Name | Type | Description | +| :----------- | :-------- | :---------------------------------------- | +| `isLast` | `boolean` | Whether this is the last page of results. | +| `offset` | `number` | Current offset in the result set. | +| `size` | `number` | Number of results in the current page. | +| `totalCount` | `number` | Total number of results available. | + +## SearchResultProduct + +| Name | Type | Description | +| :------------------------------------ | :-------------------------------------------------------------- | :------------------------------------------------------- | +| `id` | `string` | Unique product identifier. | +| `name` | `string` | Product name. | +| `npTitleId` | `string` | PlayStation Network title ID. | +| `platforms` | `string[]` | Array of platforms (e.g., `["PS4"]`, `["PS5"]`). | +| `localizedStoreDisplayClassification` | `string` | Localized product type (e.g., `"Full Game"`, `"Level"`). | +| `storeDisplayClassification` | `string` | Product classification (e.g., `"FULL_GAME"`, `"LEVEL"`). | +| `media` | [`SearchResultMedia[]`](#searchresultmedia) | Array of images and videos. | +| `personalizedMeta` | [`SearchResultPersonalizedMeta`](#searchresultpersonalizedmeta) | Personalized media content. | +| `price` | [`SearchResultSkuPrice`](#searchresultskuprice) | Pricing information. | +| `skus` | [`SearchResultSku[]`](#searchresultsku) | Available SKUs for the product. | + +## SearchResultMedia + +| Name | Type | Description | +| :----- | :------------------- | :----------------------------- | +| `role` | `string` | Media role (e.g., `"MASTER"`). | +| `type` | `"IMAGE" \| "VIDEO"` | Type of media content. | +| `url` | `string` | URL to the media file. | + +## SearchResultPersonalizedMeta + +| Name | Type | Description | +| :------------------ | :------------------------------------------ | :------------------------------------------ | +| `hasMediaOverrides` | `boolean` | Whether personalized media overrides exist. | +| `media` | [`SearchResultMedia[]`](#searchresultmedia) | Array of personalized media items. | + +## SearchResultSkuPrice + +| Name | Type | Description | +| :---------------------- |:------------------| :------------------------------------------------------------------------- | +| `skuId` | `string` | SKU identifier. | +| `basePrice` | `string \| null` | Base price with currency symbol (e.g. `"€19.99"` or `"Free"`). | +| `discountText` | `string \| null` | Discount description text. (e.g. `"-50%"`) | +| `discountedPrice` | `string \| null` | Discounted price with currency symbol. (e.g. `"€19.99"` or `"Free"`) | +| `includesBundleOffer` | `boolean \| null` | Whether a bundle offer is included. | +| `isExclusive` | `boolean` | Whether this is an exclusive product. | +| `isFree` | `boolean` | Whether the product is free. | +| `isTiedToSubscription` | `boolean \| null` | Whether tied to a subscription service. | +| `serviceBranding` | `string[]` | Service branding information. | +| `upsellServiceBranding` | `string[]` | Array of upsell service branding options. (e.g. `["NONE"]`, `["PS_PLUS"]`) | +| `upsellText` | `string \| null` | Upsell promotional text. (e.g. `"EXTRA"`) | + +## SearchResultSku + +| Name | Type | Description | +| :----- | :------- | :----------------------------- | +| `type` | `string` | SKU type (e.g., `"STANDARD"`). | diff --git a/website/docs/api-docs/universal-search.md b/website/docs/api-docs/universal-search.md index adfb5d9..fbfc84a 100644 --- a/website/docs/api-docs/universal-search.md +++ b/website/docs/api-docs/universal-search.md @@ -47,3 +47,64 @@ const response = await makeUniversalSearch( ### Source [search/makeUniversalSearch.ts](https://github.com/achievements-app/psn-api/blob/main/src/search/makeUniversalSearch.ts) + +## getSearchResults + +A call to this function will retrieve search results from the PlayStation Store based on the provided search term. This function searches for games, DLC, and other PlayStation Store products. + +:::warning +This endpoint should be called **WITHOUT** authorization to receive price information. When called with authorization, price data may be filtered or unavailable. +::: + +### Examples + +#### Search for games in the PlayStation Store + +```ts +import { getSearchResults } from "psn-api"; + +const response = await getSearchResults("batman", { + countryCode: "US", + languageCode: "en", + pageSize: 24, + pageOffset: 0 +}); +``` + +### Returns + +[`SearchResultsResponse`](/api-docs/data-models/search-results-response) + +The response contains: + +| Name | Type | Description | +| :------------------------------ | :---------------------- | :------------------------------------------------ | +| `data.universalSearch` | `object` | The main search results container | +| `data.universalSearch.next` | `string \| null` | Pagination cursor for the next page of results | +| `data.universalSearch.pageInfo` | `SearchResultPageInfo` | Pagination information (offset, size, totalCount) | +| `data.universalSearch.results` | `SearchResultProduct[]` | Array of product results with details and pricing | + +Each product result includes: + +- `id` - Product ID +- `name` - Product name +- `npTitleId` - PlayStation Network title ID +- `platforms` - Array of platforms (e.g., ["PS4", "PS5"]) +- `media` - Array of images and videos +- `price` - Pricing information including base price, discounts, etc. +- `storeDisplayClassification` - Product type (e.g., "FULL_GAME", "LEVEL") + +### Parameters + +| Name | Type | Description | +| :--------------------- | :------- | :-------------------------------------------------------------------------------- | +| `searchTerm` | `string` | The search term to query for (e.g., game name, publisher). | +| `options` | `object` | Search options including country, language, and pagination settings. | +| `options.countryCode` | `string` | Two-letter country code (e.g., "US", "GB", "NL") to determine region and pricing. | +| `options.languageCode` | `string` | Two-letter language code (e.g., "en", "es", "nl") for localized content. | +| `options.pageSize` | `number` | _(Optional)_ Number of results per page. Defaults to `24`. | +| `options.pageOffset` | `number` | _(Optional)_ Offset for pagination (0 for first page). Defaults to `0`. | + +### Source + +[graphql/getSearchResults.ts](https://github.com/achievements-app/psn-api/blob/main/src/graphql/getSearchResults.ts) From a974f9c61b4afb456c37929633fcebee06d732b4 Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Mon, 23 Feb 2026 12:32:50 +0100 Subject: [PATCH 4/7] fix: add nextCursor parameter, do not use own defaults, fix a test --- src/graphql/getSearchResults.test.ts | 5 ++--- src/graphql/getSearchResults.ts | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/graphql/getSearchResults.test.ts b/src/graphql/getSearchResults.test.ts index 3e7a4e7..b6626c8 100644 --- a/src/graphql/getSearchResults.test.ts +++ b/src/graphql/getSearchResults.test.ts @@ -66,7 +66,7 @@ describe("Function: getSearchResults", () => { isExclusive: false, isFree: false, isTiedToSubscription: null, - serviceBranding: null, + serviceBranding: [], upsellServiceBranding: [], upsellText: null }, @@ -115,7 +115,7 @@ describe("Function: getSearchResults", () => { isExclusive: false, isFree: false, isTiedToSubscription: null, - serviceBranding: null, + serviceBranding: [], upsellServiceBranding: [], upsellText: null }, @@ -141,7 +141,6 @@ describe("Function: getSearchResults", () => { countryCode: "NL", languageCode: "nl", pageSize: 2, - pageOffset: 0, searchTerm: "batman" }); const expectedExtensions = JSON.stringify({ diff --git a/src/graphql/getSearchResults.ts b/src/graphql/getSearchResults.ts index 9619910..c31da46 100644 --- a/src/graphql/getSearchResults.ts +++ b/src/graphql/getSearchResults.ts @@ -8,6 +8,7 @@ type GetSearchResultsOptions = { languageCode: string; pageSize?: number; pageOffset?: number; + nextCursor?: string; }; /** @@ -24,7 +25,7 @@ export const getSearchResults = async ( searchTerm: string, options: GetSearchResultsOptions ): Promise => { - const { countryCode, languageCode, pageSize = 24, pageOffset = 0 } = options; + const { countryCode, languageCode, pageSize, pageOffset, nextCursor } = options; const url = new URL(GRAPHQL_BASE_URL); @@ -35,7 +36,8 @@ export const getSearchResults = async ( languageCode, pageSize, pageOffset, - searchTerm + searchTerm, + nextCursor }; url.searchParams.set("variables", JSON.stringify(variables)); From da85b5cf0148af18a0f574646b817150f9c495ee Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Fri, 6 Mar 2026 19:54:23 +0100 Subject: [PATCH 5/7] docs: update api-docs and add JSDoc for getSearchResults describing option parameters --- src/graphql/getSearchResults.ts | 9 ++++++-- website/docs/api-docs/universal-search.md | 26 ++++++++++++++--------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/graphql/getSearchResults.ts b/src/graphql/getSearchResults.ts index c31da46..dfb237c 100644 --- a/src/graphql/getSearchResults.ts +++ b/src/graphql/getSearchResults.ts @@ -19,7 +19,12 @@ type GetSearchResultsOptions = { * When called with authorization, price data may be filtered or unavailable. * * @param searchTerm The search term to query for. - * @param options An object containing search options including countryCode and languageCode. + * @param options An object containing search options. + * @param options.countryCode Two-letter country code (e.g., `"US"`, `"GB"`, `"NL"`) to determine region and pricing. + * @param options.languageCode Two-letter language code (e.g., `"en"`, `"es"`, `"nl"`) for localized content. + * @param options.pageSize _(Optional)_ Number of results per page. + * @param options.pageOffset _(Optional)_ Offset for pagination (0 for first page). + * @param options.nextCursor _(Optional)_ Pagination cursor. */ export const getSearchResults = async ( searchTerm: string, @@ -34,9 +39,9 @@ export const getSearchResults = async ( const variables: Record = { countryCode, languageCode, + searchTerm, pageSize, pageOffset, - searchTerm, nextCursor }; diff --git a/website/docs/api-docs/universal-search.md b/website/docs/api-docs/universal-search.md index fbfc84a..0674d3a 100644 --- a/website/docs/api-docs/universal-search.md +++ b/website/docs/api-docs/universal-search.md @@ -7,10 +7,13 @@ sidebar_position: 2 ## makeUniversalSearch -A call to this function will make a universal search across the PlayStation Network for your search query. Each search query requires a domain, such as `"SocialAllAccounts"`. +A call to this function will make a universal search across the PlayStation Network for your search query. Each search +query requires a domain, such as `"SocialAllAccounts"`. :::note -If you search for your own username, it will not be in the list of results. This is a quirk of the universal search API. In cases where you want to use the `accountId` of the account that's currently logged in, use `"me"` instead of the standard account ID value. +If you search for your own username, it will not be in the list of results. This is a quirk of the universal search API. +In cases where you want to use the `accountId` of the account that's currently logged in, use `"me"` instead of the +standard account ID value. ::: ### Examples @@ -30,7 +33,7 @@ const response = await makeUniversalSearch( ### Returns | Name | Type | Description | -| :---------------- | :--------- | :---------- | +|:------------------|:-----------|:------------| | `prefix` | `string` | | | `suggestions` | `string[]` | | | `fallbackQueried` | `boolean` | | @@ -39,7 +42,7 @@ const response = await makeUniversalSearch( ### Parameters | Name | Type | Description | -| :-------------- | :-------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | +|:----------------|:----------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------| | `authorization` | [`AuthorizationPayload`](/api-docs/data-models/authorization-payload) | An object that must contain an `accessToken`. See [this page](/authentication/authenticating-manually) for how to get one. | | `searchTerm` | `string` | The value being searched for. | | `domain` | `string` | What kind of value is being searched for, such as `SocialAllAccounts`. | @@ -50,10 +53,12 @@ const response = await makeUniversalSearch( ## getSearchResults -A call to this function will retrieve search results from the PlayStation Store based on the provided search term. This function searches for games, DLC, and other PlayStation Store products. +A call to this function will retrieve search results from the PlayStation Store based on the provided search term. This +function searches for games, DLC, and other PlayStation Store products. :::warning -This endpoint should be called **WITHOUT** authorization to receive price information. When called with authorization, price data may be filtered or unavailable. +This endpoint should be called **WITHOUT** authorization to receive price information. When called with authorization, +price data may be filtered or unavailable. ::: ### Examples @@ -78,7 +83,7 @@ const response = await getSearchResults("batman", { The response contains: | Name | Type | Description | -| :------------------------------ | :---------------------- | :------------------------------------------------ | +|:--------------------------------|:------------------------|:--------------------------------------------------| | `data.universalSearch` | `object` | The main search results container | | `data.universalSearch.next` | `string \| null` | Pagination cursor for the next page of results | | `data.universalSearch.pageInfo` | `SearchResultPageInfo` | Pagination information (offset, size, totalCount) | @@ -97,13 +102,14 @@ Each product result includes: ### Parameters | Name | Type | Description | -| :--------------------- | :------- | :-------------------------------------------------------------------------------- | +|:-----------------------|:---------|:----------------------------------------------------------------------------------| | `searchTerm` | `string` | The search term to query for (e.g., game name, publisher). | | `options` | `object` | Search options including country, language, and pagination settings. | | `options.countryCode` | `string` | Two-letter country code (e.g., "US", "GB", "NL") to determine region and pricing. | | `options.languageCode` | `string` | Two-letter language code (e.g., "en", "es", "nl") for localized content. | -| `options.pageSize` | `number` | _(Optional)_ Number of results per page. Defaults to `24`. | -| `options.pageOffset` | `number` | _(Optional)_ Offset for pagination (0 for first page). Defaults to `0`. | +| `options.pageSize` | `number` | _(Optional)_ Number of results per page. | +| `options.pageOffset` | `number` | _(Optional)_ Offset for pagination (0 for first page). | +| `options.nextCursor` | `string` | _(Optional)_ Pagination cursor. | ### Source From c69a07dd98a81a8d561d015effa3641cafdcfdf7 Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Fri, 6 Mar 2026 20:03:18 +0100 Subject: [PATCH 6/7] test: exercise all optional parameters in tests --- src/graphql/getSearchResults.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphql/getSearchResults.test.ts b/src/graphql/getSearchResults.test.ts index b6626c8..27df64b 100644 --- a/src/graphql/getSearchResults.test.ts +++ b/src/graphql/getSearchResults.test.ts @@ -136,12 +136,14 @@ describe("Function: getSearchResults", () => { const baseUrl = `${baseUrlObj.protocol}//${baseUrlObj.host}`; const basePath = baseUrlObj.pathname; - // ... we need to use a nock matcher to verify the query parameters ... + const cursor = "CDAaVQolYjg5ODE2ODk0Y2ZiNDU2ZGIzNTU5MjIwOWM5YTdjODQ"; const expectedVariables = JSON.stringify({ countryCode: "NL", languageCode: "nl", + searchTerm: "batman", pageSize: 2, - searchTerm: "batman" + pageOffset: 0, + nextCursor: cursor }); const expectedExtensions = JSON.stringify({ persistedQuery: { @@ -151,6 +153,7 @@ describe("Function: getSearchResults", () => { } }); + // ... we need to use a nock matcher to verify the query parameters ... const mockScope = nock(baseUrl) .get(basePath) .query((params) => { @@ -165,7 +168,9 @@ describe("Function: getSearchResults", () => { const response = await getSearchResults("batman", { countryCode: "NL", languageCode: "nl", - pageSize: 2 + pageSize: 2, + pageOffset: 0, + nextCursor: cursor }); // ASSERT From 6dbf83164af152398c101ca2feba230836e77ca7 Mon Sep 17 00:00:00 2001 From: Arne Staphorsius Date: Fri, 6 Mar 2026 20:21:16 +0100 Subject: [PATCH 7/7] chore: strip undefined parameters from variables --- src/graphql/getSearchResults.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/graphql/getSearchResults.ts b/src/graphql/getSearchResults.ts index dfb237c..7e1cd6a 100644 --- a/src/graphql/getSearchResults.ts +++ b/src/graphql/getSearchResults.ts @@ -40,9 +40,9 @@ export const getSearchResults = async ( countryCode, languageCode, searchTerm, - pageSize, - pageOffset, - nextCursor + ...(pageSize !== undefined && { pageSize }), + ...(pageOffset !== undefined && { pageOffset }), + ...(nextCursor !== undefined && { nextCursor }) }; url.searchParams.set("variables", JSON.stringify(variables));