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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Changed

- Using a feature flag to route the PDP traffic to some specific accounts.

## [1.84.0] - 2025-10-07

### Changed
Expand Down
5 changes: 5 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"title": "Set to slugify links. Uses default catalog slug instead",
"type": "boolean",
"default": false
},
"shouldUseNewPDPEndpoint": {
"title": "Feature flag to use the new PDP endpoint",
"type": "boolean",
"default": false
}
}
},
Expand Down
6 changes: 5 additions & 1 deletion node/clients/intelligent-search-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { InstanceOptions, IOContext } from '@vtex/api'
import { ExternalClient } from '@vtex/api'

import { parseState } from '../utils/searchState'
import type { FetchBannersArgs, IIntelligentSearchClient } from './intsch/types'
import type { FetchBannersArgs, IIntelligentSearchClient, FetchProductArgs, FetchProductResponse } from './intsch/types'

const isPathTraversal = (str: string) => str.indexOf('..') >= 0

Expand Down Expand Up @@ -68,6 +68,10 @@ export class IntelligentSearchApi
this.locale = locale ?? tenant?.locale
}

fetchProduct(_: FetchProductArgs): Promise<FetchProductResponse> {
throw new Error('Method not implemented.')
}

public async fetchTopSearches() {
return this.http.get('/top_searches', {
params: {
Expand Down
22 changes: 19 additions & 3 deletions node/clients/intsch/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { InstanceOptions, IOContext, JanusClient } from '@vtex/api'
import type { InstanceOptions, IOContext } from '@vtex/api'
import { JanusClient } from '@vtex/api'

import {
import type {
AutocompleteSuggestionsArgs,
AutocompleteSuggestionsResponse,
CorrectionArgs,
CorrectionResponse,
FetchBannersArgs,
FetchBannersResponse,
FetchProductArgs,
FetchProductResponse,
IIntelligentSearchClient,
SearchSuggestionsArgs,
SearchSuggestionsResponse,
Expand All @@ -16,7 +19,7 @@ import {
export class Intsch extends JanusClient implements IIntelligentSearchClient {
private locale: string | undefined

public constructor(ctx: IOContext, options?: InstanceOptions) {
constructor(ctx: IOContext, options?: InstanceOptions) {
const env = ctx.production ? 'stable' : 'beta'

super(ctx, options, env)
Expand All @@ -26,6 +29,19 @@ export class Intsch extends JanusClient implements IIntelligentSearchClient {
this.locale = locale ?? tenant?.locale
}

public fetchProduct(args: FetchProductArgs): Promise<FetchProductResponse> {
return this.http.get('/api/intelligent-search/v1/products', {
params: {
field: args.field,
value: args.value,
sc: args.salesChannel ?? 1,
regionId: args.regionId,
locale: args.locale,
},
metric: 'search-product-new',
})
}

public fetchAutocompleteSuggestions(
args: AutocompleteSuggestionsArgs
): Promise<AutocompleteSuggestionsResponse> {
Expand Down
10 changes: 10 additions & 0 deletions node/clients/intsch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ export type FetchBannersResponse = {
}[]
}

export type FetchProductArgs = {
field: 'id' | 'slug' | 'ean' | 'reference' | 'sku'
value: string
salesChannel?: string
regionId?: string
locale?:string
}

export type FetchProductResponse = SearchProduct
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface IIntelligentSearchClient {
fetchAutocompleteSuggestions(
Expand All @@ -73,4 +82,5 @@ export interface IIntelligentSearchClient {
): Promise<SearchSuggestionsResponse>
fetchCorrection(args: CorrectionArgs): Promise<CorrectionResponse>
fetchBanners(args: FetchBannersArgs): Promise<FetchBannersResponse>
fetchProduct(args: FetchProductArgs): Promise<FetchProductResponse>
}
7 changes: 3 additions & 4 deletions node/directives/withSegment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { defaultFieldResolver, GraphQLField } from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'
import atob from 'atob'

/**
* Warning: we stopped using the segment client to decode the segment token for us because it added unnecessary overhead for now.
Expand All @@ -12,11 +11,11 @@ export class WithSegment extends SchemaDirectiveVisitor {
field.resolve = async (root, args, ctx: Context, info) => {
const {
vtex: { segmentToken },
clients: {segment}
clients: { segment },
} = ctx
ctx.vtex.segment = segmentToken
? JSON.parse(atob(segmentToken))
: (await segment.getSegment())
? JSON.parse(Buffer.from(segmentToken, 'base64').toString())
: await segment.getSegment()
return resolve(root, args, ctx, info)
}
}
Expand Down
3 changes: 2 additions & 1 deletion node/mocks/contextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function createContext<Ctx = Context>({
req,
query,
production,
appSettings,
}: {
production?: boolean
appSettings?: Record<string, any>
Expand Down Expand Up @@ -47,7 +48,7 @@ export function createContext<Ctx = Context>({
intelligentSearchApiSettings
),
apps: {
getAppSettings: jest.fn().mockReturnValue({}),
getAppSettings: jest.fn().mockReturnValue(appSettings ?? {}),
},
},
vtex: {
Expand Down
8 changes: 8 additions & 0 deletions node/mocks/intsch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
SearchSuggestionsResponse,
CorrectionResponse,
FetchBannersResponse,
FetchProductResponse,
} from '../clients/intsch/types'

export class MockedIntschClient implements IIntelligentSearchClient {
Expand Down Expand Up @@ -44,13 +45,19 @@ export class MockedIntschClient implements IIntelligentSearchClient {
} else {
this.fetchBanners.mockResolvedValue(args?.fetchBanners ?? null)
}
if (args?.fetchProduct instanceof Error) {
this.fetchProduct.mockRejectedValue(args.fetchProduct)
} else {
this.fetchProduct.mockResolvedValue(args?.fetchProduct ?? null)
}
}

public fetchAutocompleteSuggestions = jest.fn()
public fetchTopSearches = jest.fn()
public fetchSearchSuggestions = jest.fn()
public fetchCorrection = jest.fn()
public fetchBanners = jest.fn()
public fetchProduct = jest.fn()
}

export type IntelligentSearchClientArgs = {
Expand All @@ -59,4 +66,5 @@ export type IntelligentSearchClientArgs = {
fetchSearchSuggestions?: SearchSuggestionsResponse | Error
fetchCorrection?: CorrectionResponse | Error
fetchBanners?: FetchBannersResponse | Error
fetchProduct?: FetchProductResponse | Error
}
4 changes: 0 additions & 4 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
},
"dependencies": {
"@gocommerce/utils": "^0.6.11",
"atob": "^2.1.2",
"camelcase": "^5.0.0",
"co-body": "^6.2.0",
"cookie": "^0.3.1",
Expand All @@ -18,7 +17,6 @@
"vtex.search-graphql": "http://vtex.vtexassets.com/_v/public/typings/v1/[email protected]/public"
},
"devDependencies": {
"@types/atob": "^2.1.2",
"@types/camelcase": "^4.1.0",
"@types/co-body": "^6.1.3",
"@types/cookie": "^0.4.1",
Expand All @@ -30,8 +28,6 @@
"@vtex/api": "^7.0.0",
"@vtex/tsconfig": "^0.6.0",
"axios": "^0.21.1",
"eslint": "^5.15.3",
"eslint-config-vtex": "^10.1.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typemoq": "^2.1.0",
Expand Down
121 changes: 28 additions & 93 deletions node/resolvers/search/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { NotFoundError, UserInputError, createMessagesLoader } from '@vtex/api'
import { head, isEmpty, isNil, pathOr, test } from 'ramda'
import { pathOr, test } from 'ramda'

import {
buildAttributePath,
convertOrderBy,
} from '../../commons/compatibility-layer'
import { getWorkspaceSearchParamsFromStorage } from '../../routes/workspaceSearchParams'
import {
buildVtexSegment,
ProductArgs,
ProductIdentifier,
resolveProduct,
} from '../../services/product'
import { shouldTranslateToTenantLocale } from '../../utils/i18n'
import { resolvers as assemblyOptionResolvers } from './assemblyOption'
import { resolvers as autocompleteResolvers } from './autocomplete'
Expand Down Expand Up @@ -33,24 +39,13 @@ import {
getShippingOptionsFromSelectedFacets,
validMapAndQuery,
} from './utils'
import {
import {
fetchAutocompleteSuggestions,
fetchTopSearches,
fetchSearchSuggestions,
fetchCorrection,
} from '../../services/autocomplete'
import { fetchBanners } from '../../services/banners'
interface ProductIndentifier {
field: 'id' | 'slug' | 'ean' | 'reference' | 'sku'
value: string
}

interface ProductArgs {
slug?: string
identifier?: ProductIndentifier
regionId?: string
salesChannel?: number
}

enum CrossSellingInput {
view = 'view',
Expand All @@ -67,7 +62,7 @@ enum CrossSellingGroupByInput {
}

interface ProductRecommendationArg {
identifier?: ProductIndentifier
identifier?: ProductIdentifier
type?: CrossSellingInput
groupBy?: CrossSellingGroupByInput
}
Expand All @@ -88,25 +83,6 @@ const inputToSearchCrossSelling = {
[CrossSellingInput.suggestions]: SearchCrossSellingTypes.suggestions,
}

const buildVtexSegment = (
vtexSegment?: SegmentData,
tradePolicy?: number,
regionId?: string | null
): string => {
const cookie = {
regionId: regionId,
channel: tradePolicy,
utm_campaign: vtexSegment?.utm_campaign || '',
utm_source: vtexSegment?.utm_source || '',
utmi_campaign: vtexSegment?.utmi_campaign || '',
currencyCode: vtexSegment?.currencyCode || '',
currencySymbol: vtexSegment?.currencySymbol || '',
countryCode: vtexSegment?.countryCode || '',
cultureInfo: vtexSegment?.cultureInfo || '',
}
return new Buffer(JSON.stringify(cookie)).toString('base64')
}

/**
* There is an URL pattern in VTEX where the number of mapSegments doesn't match the number of querySegments. This function deals with these cases.
* Since this should not be a search concern, this function will be removed soon.
Expand Down Expand Up @@ -261,9 +237,6 @@ const isLegacySearchFormat = ({
return map.split(MAP_VALUES_SEP).length === query.split(PATH_SEPARATOR).length
}

const isValidProductIdentifier = (identifier: ProductIndentifier | undefined) =>
!!identifier && !isNil(identifier.value) && !isEmpty(identifier.value)

const getTranslatedSearchTerm = async (
query: SearchArgs['query'],
map: SearchArgs['map'],
Expand Down Expand Up @@ -403,61 +376,21 @@ export const queries = {
},

product: async (_: any, rawArgs: ProductArgs, ctx: Context) => {
const {
clients: { search },
} = ctx

const args =
rawArgs && isValidProductIdentifier(rawArgs.identifier)
? rawArgs
: { identifier: { field: 'slug', value: rawArgs.slug! } }

if (!args.identifier) {
throw new UserInputError('No product identifier provided')
}

let cookie: SegmentData | undefined = ctx.vtex.segment
const product = await resolveProduct(ctx, rawArgs)

const salesChannel = rawArgs.salesChannel || cookie?.channel || 1

const { field, value } = args.identifier

let products = [] as SearchProduct[]

const vtexSegment =
!cookie || (!cookie?.regionId && rawArgs.regionId)
? buildVtexSegment(cookie, salesChannel, rawArgs.regionId)
: ctx.vtex.segmentToken

switch (field) {
case 'id':
products = await search.productById(value, vtexSegment, salesChannel)
break
case 'slug':
products = await search.product(value, vtexSegment, salesChannel)
break
case 'ean':
products = await search.productByEan(value, vtexSegment, salesChannel)
break
case 'reference':
products = await search.productByReference(
value,
vtexSegment,
salesChannel
)
break
case 'sku':
products = await search.productBySku(value, vtexSegment, salesChannel)
break
}

if (products.length > 0) {
return head(products)
if (!product) {
const identifier = rawArgs?.identifier || {
field: 'slug',
value: rawArgs.slug!,
}
throw new NotFoundError(
`No product was found with requested ${
identifier.field
} ${JSON.stringify({ identifier })}`
)
}

throw new NotFoundError(
`No product was found with requested ${field} ${JSON.stringify(args)}`
)
return product
},

products: async (_: any, args: ProductsInput, ctx: Context) => {
Expand Down Expand Up @@ -516,11 +449,13 @@ export const queries = {

const vtexSegment =
!ctx.vtex.segment || (!ctx.vtex.segment?.regionId && args.regionId)
? buildVtexSegment(
ctx.vtex.segment,
Number(args.salesChannel),
args.regionId
)
? buildVtexSegment({
vtexSegment: ctx.vtex.segment as
| SegmentData
| undefined,
salesChannel: args.salesChannel?.toString(),
regionId: args.regionId ?? undefined,
})
: ctx.vtex.segmentToken

switch (field) {
Expand Down
Loading