-
Notifications
You must be signed in to change notification settings - Fork 29
fix(vnda): optimize /api/v2/tags/:name calls to prevent rate limiting #1563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,7 +13,6 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| getSEOFromTag, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| toFilters, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| toProduct, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeTagExtractor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "../utils/transform.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const VNDA_SORT_OPTIONS: SortOption[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -87,6 +86,42 @@ const handleOperator = ( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| [`${key}_operator`]: filterOperators?.[key] ?? defaultValue ?? "and", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fetchTag = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api: AppContext["api"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<Tag | undefined> => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api["GET /api/v2/tags/:name"]({ name }, STALE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((res) => res.json() as Promise<Tag>) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((): undefined => undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface TypeTag { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| key: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isProperty: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parseTypeTagsFromUrl = (url: URL): { typeTags: TypeTag[]; cleanUrl: URL } => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const TYPE_TAG_PATTERN = /^type_tags\[(.+)\]\[\]$/; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeTags = [...url.searchParams.entries()] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(([key]) => TYPE_TAG_PATTERN.test(key)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(([key, value]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const keyName = key.match(TYPE_TAG_PATTERN)?.[1] ?? ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| key, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isProperty: /^property\d+$/.test(keyName), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cleanUrl = new URL(url.href); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| [...cleanUrl.searchParams.keys()] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter((k) => k.startsWith("type_tags")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .forEach((k) => cleanUrl.searchParams.delete(k)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { typeTags, cleanUrl }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reset
Suggested fix const cleanUrl = new URL(url.href);
[...cleanUrl.searchParams.keys()]
.filter((k) => k.startsWith("type_tags"))
.forEach((k) => cleanUrl.searchParams.delete(k));
+cleanUrl.searchParams.delete("page");
return { typeTags, cleanUrl };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @title VNDA Integration | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @description Product Listing Page loader | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,44 +143,40 @@ const searchLoader = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| const isSearchPage = ctx.searchPagePath | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? ctx.searchPagePath === url.pathname | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : url.pathname === "/busca" || url.pathname === "/s"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qQueryString = url.searchParams.get("q"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const term = props.term || props.slug || qQueryString || | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const term = props.term || props.slug || qQueryString || undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const priceFilterRegex = /de-(\d+)-a-(\d+)/; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filterMatch = url.href.match(priceFilterRegex) ?? []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const categoryTagName = (props.term || url.pathname.slice(1) || "").split( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const properties1 = url.searchParams.getAll("type_tags[property1][]"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const properties2 = url.searchParams.getAll("type_tags[property2][]"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const properties3 = url.searchParams.getAll("type_tags[property3][]"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const categoryTagNames = Array.from(url.searchParams.values()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uniquePathNames = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...new Set( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| categoryTagName.filter((item): item is string => typeof item === "string"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The filter Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+153
to
+163
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use slug/path for category tag lookup, not This segment currently derives tag names from Suggested fix-const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/");
+const categoryTagName = (props.slug || url.pathname.slice(1) || "").split("/");
const uniquePathNames = [
...new Set(
- categoryTagName.filter((item): item is string => typeof item === "string"),
+ categoryTagName.filter(
+ (item): item is string => typeof item === "string" && item.trim().length > 0,
+ ),
),
];📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tags = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...categoryTagNames, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...categoryTagName.filter((item): item is string => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof item === "string" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tagByName = new Map<string, Tag | undefined>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| uniquePathNames.map( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| async (name) => [name, await fetchTag(api, name)] as const, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ].map((name) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api["GET /api/v2/tags/:name"]({ name }, STALE) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((res) => res.json()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch(() => undefined) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const categories = tags | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .slice(-categoryTagName.length) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const categories = categoryTagName | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((name) => tagByName.get(name)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter((tag): tag is Tag => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| typeof tag !== "undefined" && typeof tag.name !== "undefined" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const filteredTags = tags | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter((tag): tag is Tag => typeof tag !== "undefined"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { cleanUrl, typeTags } = typeTagExtractor(url, filteredTags); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { typeTags, cleanUrl } = parseTypeTagsFromUrl(url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const initialTags = props.tags && props.tags?.length > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? props.tags | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -165,7 +196,7 @@ const searchLoader = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| const tag = categories.at(-1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [response, seo = []] = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await api["GET /api/v2/products/search"]({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| api["GET /api/v2/products/search"]({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| term: term ?? preference, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| sort, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| page, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -211,19 +242,15 @@ const searchLoader = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) as ProductSearchResult["pagination"] | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const search = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { results: searchResults = [] } = search; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validProducts = searchResults.filter(({ variants }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return variants.length !== 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validProducts = searchResults.filter(({ variants }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variants.length !== 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const products = validProducts.map((product) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return toProduct(product, null, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| priceCurrency: "BRL", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const products = validProducts.map((product) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| toProduct(product, null, { url, priceCurrency: "BRL" }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const nextPage = new URLSearchParams(url.searchParams); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const previousPage = new URLSearchParams(url.searchParams); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -247,11 +274,7 @@ const searchLoader = async ( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| "@type": "ProductListingPage", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| seo: getSEOFromTag(categories, url, seo.at(-1), hasTypeTags, isSearchPage), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| breadcrumb: isSearchPage | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "@type": "BreadcrumbList", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| itemListElement: [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| numberOfItems: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { "@type": "BreadcrumbList", itemListElement: [], numberOfItems: 0 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : getBreadcrumbList(categories, url), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| filters: toFilters(search.aggregations, typeTags, cleanUrl), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| products, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -270,12 +293,9 @@ export const cache = "stale-while-revalidate"; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = new URL(props.pageHref || req.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const qQueryString = url.searchParams.get("q"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const term = props.term || qQueryString || | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const term = props.term || qQueryString || undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (term) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (term) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typeTags = [...url.searchParams.entries()] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(([key]) => key.includes("type_tags")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -299,19 +319,15 @@ export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ["sort", url.searchParams.get("sort") ?? props.sort ?? ""], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["type_tags", typeTags], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["tags", props?.tags?.join("\\") ?? ""], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "price", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["price", filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : ""], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["filterByTags", props.filterByTags ? "true" : "false"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["filterOperator", filterOperators.join("\\")], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["page", (url.searchParams.get("page") ?? 1).toString()], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| params.sort(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| url.search = params.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return url.href; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default searchLoader; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default searchLoader; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deno fmt --checkis failing for this file.CI is red due to formatting diffs in these changed ranges. Please run formatter before merge.
Also applies to: 153-157, 161-167, 333-333
🧰 Tools
🪛 GitHub Actions: ci
[error] 103-167: deno fmt --check failed for this file. Deno reported formatting diffs near lines 103-105, 153-157, and 161-167 (e.g., function signature/arguments and split .split("/" )/filter formatting).
🤖 Prompt for AI Agents