Skip to content

Commit 28d5f84

Browse files
committed
fix: load stage prices based on header
1 parent 5c511f0 commit 28d5f84

8 files changed

Lines changed: 116 additions & 5 deletions

src/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ declare global {
5252
info: PathInfo;
5353
content: PipelineContent;
5454
catalogPriceRules?: SharedTypes.CatalogPriceRules;
55+
stagePricing?: boolean;
5556
}
5657

5758
export interface PipelineContent extends ImportedPipelineContent {

src/product-html-pipe.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export async function productHTMLPipe(state, req) {
104104

105105
transformImages(state);
106106

107-
await fetchCatalogPriceRules(state);
107+
await fetchCatalogPriceRules(state, req);
108108
applyProductPriceRule(state, res);
109109

110110
state.timer?.update('render');

src/product-index-pipe.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export async function productIndexPipe(state, req) {
151151

152152
await setIndexCacheHeaders(state, req, res);
153153

154-
await fetchCatalogPriceRules(state);
154+
await fetchCatalogPriceRules(state, req);
155155
applyCatalogPriceRules(state, res);
156156

157157
setLastModified(state, res);

src/product-json-pipe.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export async function productJSONPipe(state, req) {
6565

6666
transformImages(state);
6767

68-
await fetchCatalogPriceRules(state);
68+
await fetchCatalogPriceRules(state, req);
6969
applyProductPriceRule(state, res);
7070

7171
// set surrogate keys

src/steps/fetch-price-rules.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
const PRICING_BUCKET_ID = 'helix-commerce-pricing';
14+
const PRICING_BUCKET_DEV_ID = 'helix-commerce-pricing-dev';
1415

1516
/**
1617
* @param {string} org
@@ -28,16 +29,27 @@ function emptyRules() {
2829

2930
/**
3031
* Fetch all catalog price rules. Sets state.catalogPriceRules.
32+
* When the request carries `x-env: stage`, fetches from the dev pricing bucket and sets
33+
* state.stagePricing = true so cache headers can be suppressed downstream.
3134
* Used for both single-product and index routes.
3235
* @param {PipelineState} state
36+
* @param {PipelineRequest} [req]
3337
*/
34-
export async function fetchCatalogPriceRules(state) {
38+
export async function fetchCatalogPriceRules(state, req) {
3539
const { org, site, s3Loader } = state;
40+
const useStagePricing = req?.headers?.get('x-env') === 'stage';
41+
const bucketId = useStagePricing
42+
? PRICING_BUCKET_DEV_ID
43+
: PRICING_BUCKET_ID;
44+
if (useStagePricing) {
45+
state.log.info('fetching catalog price rules from dev bucket');
46+
state.stagePricing = true;
47+
}
3648

3749
/** @type {PipelineResponse} */
3850
let res;
3951
try {
40-
res = await s3Loader.getObject(PRICING_BUCKET_ID, catalogRulesKey(org, site));
52+
res = await s3Loader.getObject(bucketId, catalogRulesKey(org, site));
4153
if (res.status !== 200) {
4254
state.catalogPriceRules = emptyRules();
4355
return;

src/steps/set-cache-headers.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export async function setProductCacheHeaders(state, req, resp) {
108108

109109
const productKeys = await computeProductKeys(org, site, info.path, contentBusId);
110110
setCachingHeaders(req, resp, productKeys);
111+
if (state.stagePricing) resp.headers.set('cache-control', 'no-store');
111112
}
112113

113114
/**
@@ -122,6 +123,7 @@ export async function setIndexCacheHeaders(state, req, resp) {
122123
const keys = await computeIndexKeys(org, site, info.path);
123124
keys.push(computePriceRulesKey(org, site));
124125
setCachingHeaders(req, resp, keys);
126+
if (state.stagePricing) resp.headers.set('cache-control', 'no-store');
125127
}
126128

127129
/**
@@ -136,4 +138,5 @@ export async function setSitemapCacheHeaders(state, req, resp) {
136138
const keys = await computeSitemapKeys(org, site, info.path);
137139
keys.push(computePriceRulesKey(org, site));
138140
setCachingHeaders(req, resp, keys);
141+
if (state.stagePricing) resp.headers.set('cache-control', 'no-store');
139142
}

test/steps/price-rules.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,4 +763,57 @@ describe('fetchCatalogPriceRules', () => {
763763
await fetchCatalogPriceRules(state);
764764
assert.deepStrictEqual(state.catalogPriceRules, { promotions: [] });
765765
});
766+
767+
it('uses the prod bucket and does not set stagePricing when x-env header is absent', async () => {
768+
let capturedBucket;
769+
const state = makeState({
770+
s3Loader: {
771+
async getObject(bucket) {
772+
capturedBucket = bucket;
773+
return { status: 404, body: 'Not Found' };
774+
},
775+
},
776+
});
777+
await fetchCatalogPriceRules(state, { headers: { get: () => null } });
778+
assert.strictEqual(capturedBucket, 'helix-commerce-pricing');
779+
assert.ok(!state.stagePricing, 'stagePricing must not be set');
780+
});
781+
782+
it('uses the dev bucket and sets stagePricing when x-env: stage', async () => {
783+
let capturedBucket;
784+
const state = makeState({
785+
s3Loader: {
786+
async getObject(bucket) {
787+
capturedBucket = bucket;
788+
return { status: 404, body: 'Not Found' };
789+
},
790+
},
791+
});
792+
const req = { headers: { get: (name) => (name === 'x-env' ? 'stage' : null) } };
793+
await fetchCatalogPriceRules(state, req);
794+
assert.strictEqual(capturedBucket, 'helix-commerce-pricing-dev');
795+
assert.strictEqual(state.stagePricing, true);
796+
});
797+
798+
it('reads rules from the dev bucket when x-env: stage', async () => {
799+
const rules = {
800+
promotions: [{ id: 'p', name: 'P', rules: [{ path: '/p/a', price: '9.99' }] }],
801+
};
802+
let capturedBucket;
803+
const state = makeState({
804+
s3Loader: {
805+
async getObject(bucket, key) {
806+
capturedBucket = bucket;
807+
if (bucket === 'helix-commerce-pricing-dev' && key === 'org/site/prices/catalog/rules.json') {
808+
return { status: 200, body: JSON.stringify(rules) };
809+
}
810+
return { status: 404, body: 'Not Found' };
811+
},
812+
},
813+
});
814+
const req = { headers: { get: (name) => (name === 'x-env' ? 'stage' : null) } };
815+
await fetchCatalogPriceRules(state, req);
816+
assert.strictEqual(capturedBucket, 'helix-commerce-pricing-dev');
817+
assert.strictEqual(state.catalogPriceRules.promotions.length, 1);
818+
});
766819
});

test/steps/set-cache-headers.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,22 @@ describe('setProductCacheHeaders', () => {
611611
assert.strictEqual(resp.headers.get('surrogate-control'), 'max-age=300, stale-while-revalidate=0');
612612
assert.strictEqual(resp.headers.get('surrogate-key'), 'G56lYRBKFiJX2i-A main--test-site--test-org Id9xWdjCxe493biK content-bus-id_metadata main--test-site--test-org_head content-bus-id');
613613
});
614+
615+
it('overrides cache-control to no-store when state.stagePricing is true', async () => {
616+
const state = {
617+
stagePricing: true,
618+
content: { data: { sku: 'sku', urlKey: 'url-key' } },
619+
config: { route: { params: {} } },
620+
contentBusId: 'cbus',
621+
info: { path: '/test', originalPath: '/test' },
622+
org: 'test-org',
623+
site: 'test-site',
624+
};
625+
const req = createRequest('https://example.com/test', { 'x-byo-cdn-type': 'cloudflare' });
626+
const resp = createResponse();
627+
await setProductCacheHeaders(state, req, resp);
628+
assert.strictEqual(resp.headers.get('cache-control'), 'no-store');
629+
});
614630
});
615631

616632
describe('setIndexCacheHeaders', () => {
@@ -712,6 +728,19 @@ describe('setIndexCacheHeaders', () => {
712728
assert.notStrictEqual(tags1[0], tags2[0]);
713729
assert.strictEqual(tags1[1], tags2[1]); // same site key
714730
});
731+
732+
it('overrides cache-control to no-store when state.stagePricing is true', async () => {
733+
const state = {
734+
stagePricing: true,
735+
org: 'test-org',
736+
site: 'test-site',
737+
info: { path: '/products/index.json' },
738+
};
739+
const req = createRequest('https://example.com/products/index.json', { 'x-byo-cdn-type': 'cloudflare' });
740+
const resp = createResponse();
741+
await setIndexCacheHeaders(state, req, resp);
742+
assert.strictEqual(resp.headers.get('cache-control'), 'no-store');
743+
});
715744
});
716745

717746
describe('setSitemapCacheHeaders', () => {
@@ -793,4 +822,17 @@ describe('setSitemapCacheHeaders', () => {
793822
assert.notStrictEqual(indexTags[0], sitemapTags[0]);
794823
assert.strictEqual(indexTags[1], sitemapTags[1]); // same site key
795824
});
825+
826+
it('overrides cache-control to no-store when state.stagePricing is true', async () => {
827+
const state = {
828+
stagePricing: true,
829+
org: 'test-org',
830+
site: 'test-site',
831+
info: { path: '/products/sitemap.xml' },
832+
};
833+
const req = createRequest('https://example.com/products/sitemap.xml', { 'x-byo-cdn-type': 'cloudflare' });
834+
const resp = createResponse();
835+
await setSitemapCacheHeaders(state, req, resp);
836+
assert.strictEqual(resp.headers.get('cache-control'), 'no-store');
837+
});
796838
});

0 commit comments

Comments
 (0)