Skip to content

Commit e395d8b

Browse files
Copilotbookernath
andcommitted
Add LQIP functionality to API client with image URL transformations
Co-authored-by: bookernath <8922457+bookernath@users.noreply.github.com>
1 parent 0b4f925 commit e395d8b

6 files changed

Lines changed: 400 additions & 2 deletions

File tree

packages/client/src/client.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DocumentDecoration } from './types';
66
import { getOperationInfo } from './utils/getOperationName';
77
import { normalizeQuery } from './utils/normalizeQuery';
88
import { getBackendUserAgent } from './utils/userAgent';
9+
import { transformImageUrls, ImageTransformOptions } from './utils/imageTransforms';
910

1011
export const graphqlApiDomain: string =
1112
process.env.BIGCOMMERCE_GRAPHQL_API_DOMAIN ?? 'mybigcommerce.com';
@@ -28,6 +29,10 @@ interface Config<FetcherRequestInit extends RequestInit = RequestInit> {
2829
error: BigCommerceGQLError,
2930
queryType: 'query' | 'mutation' | 'subscription',
3031
) => Promise<void> | void;
32+
/**
33+
* Configuration for automatic image transformations including LQIP
34+
*/
35+
imageTransforms?: ImageTransformOptions;
3136
}
3237

3338
interface BigCommerceResponseError {
@@ -162,6 +167,11 @@ class Client<FetcherRequestInit extends RequestInit = RequestInit> {
162167

163168
const { errors, ...data } = result;
164169

170+
// Apply image transformations if configured
171+
const transformedData = this.config.imageTransforms
172+
? { ...data, data: transformImageUrls(data.data, this.config.imageTransforms) }
173+
: data;
174+
165175
// If errorPolicy is 'none', we throw an error if there are any errors
166176
if (errorPolicy === 'none' && errors) {
167177
const error = parseGraphQLError(errors);
@@ -183,11 +193,11 @@ class Client<FetcherRequestInit extends RequestInit = RequestInit> {
183193

184194
// If errorPolicy is 'ignore', we return the data and ignore the errors
185195
if (errorPolicy === 'ignore') {
186-
return data;
196+
return transformedData;
187197
}
188198

189199
// If errorPolicy is 'all', we return the errors with the data
190-
return result;
200+
return { ...transformedData, errors };
191201
}
192202

193203
async fetchSitemapIndex(channelId?: string): Promise<string> {

packages/client/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { MissingCustomerAccessTokenError } from './missing-cat-error';
55
export { InvalidCustomerAccessTokenError } from './invalid-cat-error';
66
export { createClient } from './client';
77
export { removeEdgesAndNodes } from './utils/removeEdgesAndNodes';
8+
export { transformImageUrl, transformImageUrls, type ImageTransformOptions } from './utils/imageTransforms';
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Utility functions for image URL transformations including LQIP (Low Quality Image Placeholder) generation
3+
*/
4+
5+
/**
6+
* Interface for image transformation options
7+
*/
8+
export interface ImageTransformOptions {
9+
/**
10+
* Whether to generate lossy/compressed images for LQIP
11+
*/
12+
enableLossy?: boolean;
13+
/**
14+
* Quality setting for lossy images (1-100)
15+
*/
16+
lossyQuality?: number;
17+
}
18+
19+
/**
20+
* Default configuration for image transformations
21+
*/
22+
export const DEFAULT_IMAGE_TRANSFORM_OPTIONS: Required<ImageTransformOptions> = {
23+
enableLossy: true,
24+
lossyQuality: 30,
25+
};
26+
27+
/**
28+
* Transform a BigCommerce image URL to include lossy parameters for LQIP
29+
*
30+
* @param url - The original image URL
31+
* @param options - Transform options
32+
* @returns The transformed URL with lossy parameters
33+
*/
34+
export function transformImageUrl(url: string, options: ImageTransformOptions = {}): string {
35+
if (!url || typeof url !== 'string') {
36+
return url;
37+
}
38+
39+
const config = { ...DEFAULT_IMAGE_TRANSFORM_OPTIONS, ...options };
40+
41+
if (!config.enableLossy) {
42+
return url;
43+
}
44+
45+
// Check if this is a BigCommerce CDN URL that can be transformed
46+
if (!isBigCommerceCdnUrl(url)) {
47+
return url;
48+
}
49+
50+
// If URL already has lossy parameters, don't transform again
51+
if (url.includes('lossy=') || url.includes('quality=')) {
52+
return url;
53+
}
54+
55+
// Add lossy parameter to the URL
56+
const separator = url.includes('?') ? '&' : '?';
57+
return `${url}${separator}lossy=true&quality=${config.lossyQuality}`;
58+
}
59+
60+
/**
61+
* Check if a URL is from the BigCommerce CDN and can be transformed
62+
*
63+
* @param url - The URL to check
64+
* @returns True if the URL can be transformed
65+
*/
66+
function isBigCommerceCdnUrl(url: string): boolean {
67+
if (!url) return false;
68+
69+
// Check for BigCommerce CDN patterns
70+
const cdnPatterns = [
71+
/cdn\d*\.bigcommerce\.com/,
72+
/bigcommerce-.*\.s3\.amazonaws\.com/,
73+
/store-.*\.mybigcommerce\.com/,
74+
];
75+
76+
return cdnPatterns.some(pattern => pattern.test(url));
77+
}
78+
79+
/**
80+
* Transform image objects in API responses to include LQIP URLs
81+
*
82+
* @param data - The API response data
83+
* @param options - Transform options
84+
* @returns Transformed data with LQIP URLs
85+
*/
86+
export function transformImageUrls(data: any, options: ImageTransformOptions = {}): any {
87+
if (!data || typeof data !== 'object') {
88+
return data;
89+
}
90+
91+
if (Array.isArray(data)) {
92+
return data.map(item => transformImageUrls(item, options));
93+
}
94+
95+
const result = { ...data };
96+
97+
// Transform specific image URL fields
98+
const imageUrlFields = ['url', 'src', 'urlOriginal', 'urlStandard', 'urlThumbnail'];
99+
100+
for (const field of imageUrlFields) {
101+
if (result[field] && typeof result[field] === 'string') {
102+
result[field] = transformImageUrl(result[field], options);
103+
}
104+
}
105+
106+
// Transform nested objects
107+
for (const [key, value] of Object.entries(result)) {
108+
if (value && typeof value === 'object') {
109+
result[key] = transformImageUrls(value, options);
110+
}
111+
}
112+
113+
return result;
114+
}

packages/client/test-runner.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env node
2+
3+
// Simple test runner for image transforms
4+
const { transformImageUrl, transformImageUrls, DEFAULT_IMAGE_TRANSFORM_OPTIONS } = require('./src/utils/imageTransforms');
5+
6+
console.log('Testing image transforms...');
7+
8+
// Test 1: Basic URL transformation
9+
const testUrl = 'https://cdn11.bigcommerce.com/s-123abc/products/123/images/456/image.jpg';
10+
const transformed = transformImageUrl(testUrl);
11+
const expected = `${testUrl}?lossy=true&quality=${DEFAULT_IMAGE_TRANSFORM_OPTIONS.lossyQuality}`;
12+
13+
console.log('Test 1 - Basic URL transformation:');
14+
console.log(' Input:', testUrl);
15+
console.log(' Output:', transformed);
16+
console.log(' Expected:', expected);
17+
console.log(' ✓ Pass:', transformed === expected);
18+
19+
// Test 2: Non-BigCommerce URL (should not transform)
20+
const externalUrl = 'https://example.com/image.jpg';
21+
const untransformed = transformImageUrl(externalUrl);
22+
23+
console.log('\nTest 2 - External URL (should not transform):');
24+
console.log(' Input:', externalUrl);
25+
console.log(' Output:', untransformed);
26+
console.log(' ✓ Pass:', untransformed === externalUrl);
27+
28+
// Test 3: Nested object transformation
29+
const testData = {
30+
product: {
31+
name: 'Test Product',
32+
image: {
33+
url: 'https://cdn11.bigcommerce.com/s-123abc/products/123/images/456/image.jpg'
34+
}
35+
}
36+
};
37+
38+
const transformedData = transformImageUrls(testData);
39+
40+
console.log('\nTest 3 - Nested object transformation:');
41+
console.log(' Original URL:', testData.product.image.url);
42+
console.log(' Transformed URL:', transformedData.product.image.url);
43+
console.log(' ✓ Pass:', transformedData.product.image.url.includes('lossy=true'));
44+
45+
console.log('\nAll tests completed!');

packages/client/test-simple.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Simple JavaScript version for testing
2+
const DEFAULT_IMAGE_TRANSFORM_OPTIONS = {
3+
enableLossy: true,
4+
lossyQuality: 30,
5+
};
6+
7+
function isBigCommerceCdnUrl(url) {
8+
if (!url) return false;
9+
10+
const cdnPatterns = [
11+
/cdn\d*\.bigcommerce\.com/,
12+
/bigcommerce-.*\.s3\.amazonaws\.com/,
13+
/store-.*\.mybigcommerce\.com/,
14+
];
15+
16+
return cdnPatterns.some(pattern => pattern.test(url));
17+
}
18+
19+
function transformImageUrl(url, options = {}) {
20+
if (!url || typeof url !== 'string') {
21+
return url;
22+
}
23+
24+
const config = { ...DEFAULT_IMAGE_TRANSFORM_OPTIONS, ...options };
25+
26+
if (!config.enableLossy) {
27+
return url;
28+
}
29+
30+
if (!isBigCommerceCdnUrl(url)) {
31+
return url;
32+
}
33+
34+
if (url.includes('lossy=') || url.includes('quality=')) {
35+
return url;
36+
}
37+
38+
const separator = url.includes('?') ? '&' : '?';
39+
return `${url}${separator}lossy=true&quality=${config.lossyQuality}`;
40+
}
41+
42+
function transformImageUrls(data, options = {}) {
43+
if (!data || typeof data !== 'object') {
44+
return data;
45+
}
46+
47+
if (Array.isArray(data)) {
48+
return data.map(item => transformImageUrls(item, options));
49+
}
50+
51+
const result = { ...data };
52+
const imageUrlFields = ['url', 'src', 'urlOriginal', 'urlStandard', 'urlThumbnail'];
53+
54+
for (const field of imageUrlFields) {
55+
if (result[field] && typeof result[field] === 'string') {
56+
result[field] = transformImageUrl(result[field], options);
57+
}
58+
}
59+
60+
for (const [key, value] of Object.entries(result)) {
61+
if (value && typeof value === 'object') {
62+
result[key] = transformImageUrls(value, options);
63+
}
64+
}
65+
66+
return result;
67+
}
68+
69+
// Test the functionality
70+
console.log('Testing image transforms...');
71+
72+
// Test 1: Basic URL transformation
73+
const testUrl = 'https://cdn11.bigcommerce.com/s-123abc/products/123/images/456/image.jpg';
74+
const transformed = transformImageUrl(testUrl);
75+
const expected = `${testUrl}?lossy=true&quality=${DEFAULT_IMAGE_TRANSFORM_OPTIONS.lossyQuality}`;
76+
77+
console.log('Test 1 - Basic URL transformation:');
78+
console.log(' Input:', testUrl);
79+
console.log(' Output:', transformed);
80+
console.log(' Expected:', expected);
81+
console.log(' ✓ Pass:', transformed === expected);
82+
83+
// Test 2: Non-BigCommerce URL (should not transform)
84+
const externalUrl = 'https://example.com/image.jpg';
85+
const untransformed = transformImageUrl(externalUrl);
86+
87+
console.log('\nTest 2 - External URL (should not transform):');
88+
console.log(' Input:', externalUrl);
89+
console.log(' Output:', untransformed);
90+
console.log(' ✓ Pass:', untransformed === externalUrl);
91+
92+
// Test 3: URL with existing query params
93+
const urlWithParams = 'https://cdn11.bigcommerce.com/s-123abc/products/123/images/456/image.jpg?width=100';
94+
const transformedWithParams = transformImageUrl(urlWithParams);
95+
const expectedWithParams = `${urlWithParams}&lossy=true&quality=${DEFAULT_IMAGE_TRANSFORM_OPTIONS.lossyQuality}`;
96+
97+
console.log('\nTest 3 - URL with existing query params:');
98+
console.log(' Input:', urlWithParams);
99+
console.log(' Output:', transformedWithParams);
100+
console.log(' Expected:', expectedWithParams);
101+
console.log(' ✓ Pass:', transformedWithParams === expectedWithParams);
102+
103+
// Test 4: Nested object transformation
104+
const testData = {
105+
product: {
106+
name: 'Test Product',
107+
image: {
108+
url: 'https://cdn11.bigcommerce.com/s-123abc/products/123/images/456/image.jpg'
109+
}
110+
}
111+
};
112+
113+
const transformedData = transformImageUrls(testData);
114+
115+
console.log('\nTest 4 - Nested object transformation:');
116+
console.log(' Original URL:', testData.product.image.url);
117+
console.log(' Transformed URL:', transformedData.product.image.url);
118+
console.log(' ✓ Pass:', transformedData.product.image.url.includes('lossy=true'));
119+
120+
// Test 5: Disabled lossy
121+
const disabledTransform = transformImageUrl(testUrl, { enableLossy: false });
122+
123+
console.log('\nTest 5 - Disabled lossy:');
124+
console.log(' Input:', testUrl);
125+
console.log(' Output:', disabledTransform);
126+
console.log(' ✓ Pass:', disabledTransform === testUrl);
127+
128+
console.log('\nAll tests completed!');

0 commit comments

Comments
 (0)