Skip to content

Commit 1da7bdb

Browse files
whitelisabclaude
andcommitted
fix(salesforce-commerce-cloud): trim sfccApi responses to stay under App Action response cap
sfccApi was relaying full SFCC product/category representations verbatim, exceeding the App Framework's 400KB response limit. Project responses down to the fields the UI actually reads. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 7a7917e commit 1da7bdb

2 files changed

Lines changed: 179 additions & 4 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { describe, expect, it, vi, beforeEach } from 'vitest';
2+
import { handler } from './index';
3+
4+
const installationParameters = {
5+
clientId: 'client-id',
6+
clientSecret: 'client-secret',
7+
organizationId: 'f_ecom_zzte_053',
8+
shortCode: 'abcd1234',
9+
siteId: 'RefArchGlobal',
10+
};
11+
12+
function mockContext() {
13+
return { appInstallationParameters: installationParameters } as any;
14+
}
15+
16+
function mockTokenResponse() {
17+
return { ok: true, json: async () => ({ access_token: 'test-token' }) } as Response;
18+
}
19+
20+
// Simulates the kind of full SFCC representation that triggers max-response-size-exceeded:
21+
// a couple of fields the UI actually renders, plus some nested data it never reads.
22+
function bulkyProduct(id: string) {
23+
return {
24+
id,
25+
name: { default: `Product ${id}` },
26+
image: { absUrl: `https://example.com/${id}.jpg`, alt: { default: 'alt text' } },
27+
shortDescription: { default: { markup: 'x'.repeat(50_000) } },
28+
variations: new Array(50).fill({ some: 'nested variation data the UI never reads' }),
29+
};
30+
}
31+
32+
function bulkyCategory(id: string) {
33+
return {
34+
id,
35+
catalogId: 'catalog-1',
36+
name: { default: `Category ${id}` },
37+
pageDescription: { default: 'y'.repeat(50_000) },
38+
paths: new Array(50).fill({ id: 'catalog-1', name: { default: 'Catalog One' } }),
39+
};
40+
}
41+
42+
describe('sfccApi function handler', () => {
43+
beforeEach(() => {
44+
vi.restoreAllMocks();
45+
});
46+
47+
it('trims searchProducts hits to id/name/image', async () => {
48+
const fetchMock = vi.fn(async (url: string) => {
49+
if (url.includes('dwsso/oauth2/access_token')) return mockTokenResponse();
50+
if (url.includes('/product-search')) {
51+
expect(url).not.toContain('limit=');
52+
return { ok: true, json: async () => ({ hits: [bulkyProduct('prod-1')] }) } as Response;
53+
}
54+
throw new Error(`Unexpected fetch URL: ${url}`);
55+
});
56+
vi.stubGlobal('fetch', fetchMock);
57+
58+
const result: any = await handler(
59+
{ body: { type: 'searchProducts', query: 'test' } } as any,
60+
mockContext()
61+
);
62+
63+
expect(result.ok).toBe(true);
64+
expect(result.data).toEqual([
65+
{
66+
id: 'prod-1',
67+
name: { default: 'Product prod-1' },
68+
image: { absUrl: 'https://example.com/prod-1.jpg', alt: { default: 'alt text' } },
69+
},
70+
]);
71+
expect(Buffer.byteLength(JSON.stringify(result.data))).toBeLessThan(1000);
72+
});
73+
74+
it('trims searchCategories hits to id/catalogId/name', async () => {
75+
const fetchMock = vi.fn(async (url: string) => {
76+
if (url.includes('dwsso/oauth2/access_token')) return mockTokenResponse();
77+
if (url.includes('/category-search')) {
78+
expect(url).not.toContain('limit=');
79+
return { ok: true, json: async () => ({ hits: [bulkyCategory('cat-1')] }) } as Response;
80+
}
81+
throw new Error(`Unexpected fetch URL: ${url}`);
82+
});
83+
vi.stubGlobal('fetch', fetchMock);
84+
85+
const result: any = await handler(
86+
{ body: { type: 'searchCategories', query: 'test' } } as any,
87+
mockContext()
88+
);
89+
90+
expect(result.ok).toBe(true);
91+
expect(result.data).toEqual([
92+
{ id: 'cat-1', catalogId: 'catalog-1', name: { default: 'Category cat-1' } },
93+
]);
94+
expect(Buffer.byteLength(JSON.stringify(result.data))).toBeLessThan(1000);
95+
});
96+
97+
it('trims fetchProduct response to id/name/image', async () => {
98+
const fetchMock = vi.fn(async (url: string) => {
99+
if (url.includes('dwsso/oauth2/access_token')) return mockTokenResponse();
100+
if (url.includes('/products/prod-1')) {
101+
return { ok: true, json: async () => bulkyProduct('prod-1') } as Response;
102+
}
103+
throw new Error(`Unexpected fetch URL: ${url}`);
104+
});
105+
vi.stubGlobal('fetch', fetchMock);
106+
107+
const result: any = await handler(
108+
{ body: { type: 'fetchProduct', productId: 'prod-1' } } as any,
109+
mockContext()
110+
);
111+
112+
expect(result.ok).toBe(true);
113+
expect(result.data).toEqual({
114+
id: 'prod-1',
115+
name: { default: 'Product prod-1' },
116+
image: { absUrl: 'https://example.com/prod-1.jpg', alt: { default: 'alt text' } },
117+
});
118+
});
119+
120+
it('trims fetchCategory response to id/catalogId/name', async () => {
121+
const fetchMock = vi.fn(async (url: string) => {
122+
if (url.includes('dwsso/oauth2/access_token')) return mockTokenResponse();
123+
if (url.includes('/categories/cat-1')) {
124+
return { ok: true, json: async () => bulkyCategory('cat-1') } as Response;
125+
}
126+
throw new Error(`Unexpected fetch URL: ${url}`);
127+
});
128+
vi.stubGlobal('fetch', fetchMock);
129+
130+
const result: any = await handler(
131+
{ body: { type: 'fetchCategory', catalogId: 'catalog-1', categoryId: 'cat-1' } } as any,
132+
mockContext()
133+
);
134+
135+
expect(result.ok).toBe(true);
136+
expect(result.data).toEqual({
137+
id: 'cat-1',
138+
catalogId: 'catalog-1',
139+
name: { default: 'Category cat-1' },
140+
});
141+
});
142+
});

apps/salesforce-commerce-cloud/functions/index.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,39 @@ interface InstallationParameters {
1919
siteId: string;
2020
}
2121

22+
interface SfccLocalizedText {
23+
default?: string;
24+
}
25+
26+
interface SfccImage {
27+
absUrl?: string;
28+
alt?: SfccLocalizedText;
29+
}
30+
31+
interface ProductSummary {
32+
id: string;
33+
name?: SfccLocalizedText;
34+
image?: SfccImage;
35+
}
36+
37+
interface CategorySummary {
38+
id: string;
39+
catalogId?: string;
40+
name?: SfccLocalizedText;
41+
}
42+
43+
// The UI only ever reads id/name/image (products) or id/catalogId/name (categories) —
44+
// see SearchBar, ProductSearchResults, CategorySearchResults, ItemCard. Projecting down
45+
// to those fields here keeps every sfccApi response far under the App Action platform's
46+
// response size cap, regardless of how much SFCC includes in the full representation.
47+
function toProductSummary(product: any): ProductSummary {
48+
return { id: product?.id, name: product?.name, image: product?.image };
49+
}
50+
51+
function toCategorySummary(category: any): CategorySummary {
52+
return { id: category?.id, catalogId: category?.catalogId, name: category?.name };
53+
}
54+
2255
async function fetchSfccToken(params: InstallationParameters): Promise<string> {
2356
const authToken = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');
2457
const tenantId = params.organizationId.split('_').slice(2).join('_');
@@ -110,7 +143,7 @@ export const handler: FunctionEventHandler<FunctionTypeEnum.AppActionCall> = asy
110143
method: 'POST',
111144
body: JSON.stringify(buildSearchBody(action.query)),
112145
})) as { hits?: unknown[] };
113-
return { ok: true, data: data.hits ?? [] };
146+
return { ok: true, data: (data.hits ?? []).map(toProductSummary) };
114147
}
115148

116149
case 'searchCategories': {
@@ -119,19 +152,19 @@ export const handler: FunctionEventHandler<FunctionTypeEnum.AppActionCall> = asy
119152
method: 'POST',
120153
body: JSON.stringify(buildSearchBody(action.query, true)),
121154
})) as { hits?: unknown[] };
122-
return { ok: true, data: data.hits ?? [] };
155+
return { ok: true, data: (data.hits ?? []).map(toCategorySummary) };
123156
}
124157

125158
case 'fetchProduct': {
126159
const url = `${base}/product/products/v1/organizations/${organizationId}/products/${action.productId}?siteId=${siteId}`;
127160
const data = await sfccFetch(url, token, { method: 'GET' });
128-
return { ok: true, data };
161+
return { ok: true, data: toProductSummary(data) };
129162
}
130163

131164
case 'fetchCategory': {
132165
const url = `${base}/product/catalogs/v1/organizations/${organizationId}/catalogs/${action.catalogId}/categories/${action.categoryId}`;
133166
const data = await sfccFetch(url, token, { method: 'GET' });
134-
return { ok: true, data };
167+
return { ok: true, data: toCategorySummary(data) };
135168
}
136169

137170
default:

0 commit comments

Comments
 (0)