Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions e2e/tests/productV1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,3 +1030,258 @@ describe("core-sdk-check-exchange-policy", () => {
expect(result.errors[0].value).toEqual("unfairExchangePolicy");
});
});

describe("search products tests", () => {
let coreSDK: CoreSDK;
// Use random keywords to allow test to be repeated again and again
const random = Math.floor(Math.random() * 10000);
// Define 3 keywords and use them in different fields of product metadata
// (name, description, tags) to test that search is working on all these fields
const keywords = [
`Keyword_1_${random}`,
`Keyword_2_${random}`,
`Keyword_3_${random}`
];
const voidedOfferUuid = buildUuid();
const productMetadatas: {
uuid: string;
name: string;
description: string;
tags: string[];
}[] = [
{
uuid: buildUuid(),
name: `${keywords[0]} red shirt`,
description: "a red shirt",
tags: ["clothing", "red", "shirt"]
},
{
uuid: buildUuid(),
name: `blue shirt`,
description: `${keywords[1]} a blue shirt`,
tags: ["clothing", "blue", "shirt"]
},
{
uuid: buildUuid(),
name: "green shirt",
description: "a green shirt",
tags: ["clothing", "green", "shirt", keywords[2]]
},
{
uuid: buildUuid(),
name: `${keywords[0]} white shirt`,
description: `a white shirt ${keywords[1]}`,
tags: ["clothing", "white", "shirt", keywords[2]]
},
{
uuid: buildUuid(),
name: `rainbow shirt ${keywords[0]} ${keywords[1]} ${keywords[2]}`,
description: `a rainbow shirt ${keywords[1]} ${keywords[2]} ${keywords[0]} `,
tags: [
"clothing",
"rainbow",
"shirt",
keywords[2],
keywords[1],
keywords[0]
]
},
{
uuid: voidedOfferUuid,
name: `voided offer ${keywords[0]} ${keywords[1]} ${keywords[2]}`,
description: `a voided offer ${keywords[1]} ${keywords[2]} ${keywords[0]} `,
tags: [
"clothing",
"rainbow",
"shirt",
keywords[2],
keywords[1],
keywords[0]
]
},
{
uuid: buildUuid(),
name: "black shirt",
description: "a black shirt",
tags: ["clothing", "black", "shirt"]
}
];
beforeAll(async () => {
let sellerWallet: Wallet;
({ coreSDK, fundedWallet: sellerWallet } =
await initCoreSDKWithFundedWallet(seedWallet));
const template =
"Hello World!! {{sellerTradingName}} {{disputeResolverContactMethod}} {{sellerContactMethod}} {{returnPeriodInDays}}";
const offerArgsList: CreateOfferArgs[] = [];
for (const productMetadata of productMetadatas) {
const metadata = mockProductV1Metadata(template, productMetadata.uuid, {
name: productMetadata.name,
description: productMetadata.description,
product: {
title: productMetadata.name,
description: productMetadata.description,
uuid: productMetadata.uuid,
details_tags: productMetadata.tags
}
});
const offerArgs = await createOfferArgs(coreSDK, metadata);
resolveDateValidity(offerArgs);
offerArgsList.push(offerArgs);
}
const offers = await createOfferBatch(coreSDK, sellerWallet, offerArgsList);
expect(offers.length).toEqual(productMetadatas.length);
const voidedOfferId = offers.find(
(offer) =>
(offer.metadata as { product: { uuid: string } })?.product?.uuid ===
voidedOfferUuid
)?.id;
expect(voidedOfferId).toBeTruthy();
const tx = await coreSDK.voidOffer(voidedOfferId as string);
await coreSDK.waitForGraphNodeIndexing(tx);
});
test("search with a unused keyword should not find any products", async () => {
const results = await coreSDK.searchProducts(["unused_keyword"]);
expect(results.length).toEqual(0);
});
test("search with a keyword put in product name should find matching products", async () => {
const results = await coreSDK.searchProducts([keywords[0]]);
expect(results.length).toEqual(3);
let foundProduct = results.find(
(result) => result.uuid === productMetadatas[0].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[3].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[4].uuid
);
expect(foundProduct).toBeTruthy();
});
test("search with a keyword put in product description should find matching products", async () => {
const results = await coreSDK.searchProducts([keywords[1]]);
expect(results.length).toEqual(3);
let foundProduct = results.find(
(result) => result.uuid === productMetadatas[1].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[3].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[4].uuid
);
expect(foundProduct).toBeTruthy();
});
test("search with a keyword put in product tags should find matching products", async () => {
const results = await coreSDK.searchProducts([keywords[2]]);
expect(results.length).toEqual(3);
let foundProduct = results.find(
(result) => result.uuid === productMetadatas[2].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[3].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[4].uuid
);
expect(foundProduct).toBeTruthy();
});
test("search with multiple keywords should find all matching products - 1", async () => {
const results = await coreSDK.searchProducts([keywords[0], keywords[1]]);
expect(results.length).toEqual(4);
let foundProduct = results.find(
(result) => result.uuid === productMetadatas[0].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[1].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[3].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[4].uuid
);
expect(foundProduct).toBeTruthy();
});
test("search with multiple keywords should find all matching products - 2", async () => {
const results = await coreSDK.searchProducts([keywords[2], keywords[1]]);
expect(results.length).toEqual(4);
let foundProduct = results.find(
(result) => result.uuid === productMetadatas[2].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[1].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[3].uuid
);
expect(foundProduct).toBeTruthy();
foundProduct = results.find(
(result) => result.uuid === productMetadatas[4].uuid
);
expect(foundProduct).toBeTruthy();
});
test("search with all keywords should find all matching products", async () => {
const results = await coreSDK.searchProducts(keywords, {
includeInvalidOffers: false
});
expect(results.length).toEqual(5);
for (let i = 0; i < 5; i++) {
const foundProduct = results.find(
(result) => result.uuid === productMetadatas[i].uuid
);
expect(foundProduct).toBeTruthy();
}
});
test("search with pagination", async () => {
let results = await coreSDK.searchProducts(keywords, {
productsFirst: 4,
productsSkip: 0
});
expect(results.length).toEqual(4);
results = await coreSDK.searchProducts(keywords, {
productsFirst: 4,
productsSkip: 4
});
expect(results.length).toEqual(1);
});
test("search with additional criteria", async () => {
let results = await coreSDK.searchProducts(keywords, {
productsFilter: {
uuid_in: [
productMetadatas[0].uuid,
productMetadatas[1].uuid,
voidedOfferUuid
]
}
});
expect(results.length).toEqual(2);

results = await coreSDK.searchProducts(keywords, {
productsFilter: {
uuid_in: [
productMetadatas[0].uuid,
productMetadatas[1].uuid,
voidedOfferUuid
]
},
includeInvalidOffers: true
});
expect(results.length).toEqual(3);
});
test("search with empty keywords should return no results", async () => {
const results = await coreSDK.searchProducts([]);
expect(Array.isArray(results)).toBe(true);
expect(results.length).toEqual(0);
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"update-contract-uri": "ts-node -P tsconfig.base.json ./scripts/change-contract-uri.ts",
"check-offer-exchange-policy": "ts-node -P tsconfig.base.json ./scripts/checkOfferExchangePolicy.ts",
"get-buyers": "ts-node -P tsconfig.base.json ./scripts/get-buyers.ts",
"search-products": "ts-node -P tsconfig.base.json ./scripts/search-products.ts",
"ts-coverage": "typescript-coverage-report",
"update-protocol-addresses": "ts-node -P tsconfig.base.json ./scripts/update-protocol-addresses.ts"
},
Expand Down
7 changes: 5 additions & 2 deletions packages/core-sdk/src/core-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { ErrorMixin } from "./errors/mixin";
import { SubgraphMixin } from "./subgraph/mixin";
import { PriceDiscoveryMixin } from "./price-discovery/mixin";
import { MarketplaceMixin } from "./marketplaces/mixin";
import { SearchMixin } from "./search/mixin";

export class CoreSDK<T extends Web3LibAdapter> extends BaseCoreSDK<T> {
/**
Expand Down Expand Up @@ -153,7 +154,8 @@ export interface CoreSDK<T extends Web3LibAdapter = Web3LibAdapter>
ErrorMixin<T>,
SubgraphMixin<T>,
PriceDiscoveryMixin<T>,
MarketplaceMixin<T> {}
MarketplaceMixin<T>,
SearchMixin<T> {}
applyMixins(CoreSDK, [
MetadataMixin,
AccountsMixin,
Expand All @@ -175,5 +177,6 @@ applyMixins(CoreSDK, [
ErrorMixin,
SubgraphMixin,
PriceDiscoveryMixin,
MarketplaceMixin
MarketplaceMixin,
SearchMixin
]);
1 change: 1 addition & 0 deletions packages/core-sdk/src/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as subgraph from "./subgraph";
69 changes: 69 additions & 0 deletions packages/core-sdk/src/search/mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Web3LibAdapter } from "@bosonprotocol/common";
import { BaseCoreSDK } from "../mixins/base-core-sdk";
import { searchProducts } from "./subgraph";
import * as subgraph from "../subgraph";

export class SearchMixin<T extends Web3LibAdapter> extends BaseCoreSDK<T> {
/**
* Search for products matching the given keywords.
*
* By default, only products that are currently valid (based on their validity dates) are returned.
* This behavior can be controlled via the `includeInvalidOffers` option in the queryVars parameter.
*
* @param keywords - List of keywords to match against product title, description, and tags.
* @param queryVars - Optional query variables including pagination (productsSkip, productsFirst),
* ordering (productsOrderBy, productsOrderDirection), filtering (productsFilter),
* and includeInvalidOffers flag to control validity filtering.
* @returns A promise that resolves to an array of product search result fragments.
*/
public async searchProducts(
keywords: string[],
queryVars?: subgraph.SearchProductsQueryQueryVariables & {
includeInvalidOffers?: boolean;
}
): Promise<subgraph.ProductSearchResultFieldsFragment[]> {
if (!keywords || keywords.length === 0) {
return [];
}
const now = Math.floor(Date.now() / 1000);
const validOffersFilter: subgraph.ProductV1Product_Filter = {
allVariantsVoided_not: true,
minValidFromDate_lte: now + 60 + "", // Add 1 minute to ensure we include offers not valid yet, but valid in a very little time
maxValidUntilDate_gte: now + ""
};
const productsSearchFilter: subgraph.ProductV1Product_Filter = {
or: keywords
.map((keyword) => [
{ title_contains_nocase: keyword },
{ description_contains_nocase: keyword },
{ details_tags_contains_nocase: [keyword] }
])
.flat()
};
const productsFilter: subgraph.ProductV1Product_Filter =
queryVars?.includeInvalidOffers
? queryVars?.productsFilter
? {
and: [
{
...queryVars.productsFilter
},
productsSearchFilter
]
}
: productsSearchFilter
: {
and: [
{
...validOffersFilter,
...queryVars?.productsFilter
},
productsSearchFilter
]
};
return searchProducts(this._subgraphUrl, {
...queryVars,
productsFilter
});
Comment on lines +64 to +67
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The searchProducts query does not set default values for pagination parameters (productsFirst, productsSkip). Without these, the query could potentially return a very large number of results, which could cause performance issues or timeouts. Consider adding reasonable default values, such as a limit of 100 or 1000 results, or documenting that callers should set these parameters for production use.

Copilot uses AI. Check for mistakes.
}
}
Loading
Loading