musora-content-services 2.3.2 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/src/index.d.ts +18 -0
- package/src/index.js +18 -0
- package/src/services/content.js +0 -0
- package/src/services/forum.js +8 -4
- package/src/services/imageSRCBuilder.js +139 -0
- package/src/services/imageSRCVerify.js +114 -0
- package/src/services/railcontent.js +3 -3
- package/src/services/userActivity.js +7 -9
- package/test/imageSRCBuilder.test.js +37 -0
- package/test/imageSRCVerify.test.js +160 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.3.4](https://github.com/railroadmedia/musora-content-services/compare/v2.3.2...v2.3.4) (2025-04-03)
|
|
6
|
+
|
|
7
|
+
### [2.3.3](https://github.com/railroadmedia/musora-content-services/compare/v2.3.2...v2.3.3) (2025-04-02)
|
|
8
|
+
|
|
5
9
|
### [2.3.2](https://github.com/railroadmedia/musora-content-services/compare/v2.3.1...v2.3.2) (2025-03-31)
|
|
6
10
|
|
|
7
11
|
### [2.3.1](https://github.com/railroadmedia/musora-content-services/compare/v2.3.0...v2.3.1) (2025-03-31)
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -55,6 +55,18 @@ import {
|
|
|
55
55
|
fetchAwardsForUser
|
|
56
56
|
} from './services/gamification/awards.js';
|
|
57
57
|
|
|
58
|
+
import {
|
|
59
|
+
applyCloudflareWrapper,
|
|
60
|
+
applySanityTransformations,
|
|
61
|
+
buildImageSRC
|
|
62
|
+
} from './services/imageSRCBuilder.js';
|
|
63
|
+
|
|
64
|
+
import {
|
|
65
|
+
extractSanityUrl,
|
|
66
|
+
isBucketUrl,
|
|
67
|
+
verifyImageSRC
|
|
68
|
+
} from './services/imageSRCVerify.js';
|
|
69
|
+
|
|
58
70
|
import {
|
|
59
71
|
assignModeratorToComment,
|
|
60
72
|
closeComment,
|
|
@@ -214,10 +226,13 @@ import {
|
|
|
214
226
|
declare module 'musora-content-services' {
|
|
215
227
|
export {
|
|
216
228
|
addItemToPlaylist,
|
|
229
|
+
applyCloudflareWrapper,
|
|
230
|
+
applySanityTransformations,
|
|
217
231
|
assignModeratorToComment,
|
|
218
232
|
assignmentStatusCompleted,
|
|
219
233
|
assignmentStatusReset,
|
|
220
234
|
blockUser,
|
|
235
|
+
buildImageSRC,
|
|
221
236
|
closeComment,
|
|
222
237
|
contentStatusCompleted,
|
|
223
238
|
contentStatusReset,
|
|
@@ -231,6 +246,7 @@ declare module 'musora-content-services' {
|
|
|
231
246
|
deletePracticeSession,
|
|
232
247
|
duplicatePlaylist,
|
|
233
248
|
editComment,
|
|
249
|
+
extractSanityUrl,
|
|
234
250
|
fetchAll,
|
|
235
251
|
fetchAllCompletedStates,
|
|
236
252
|
fetchAllFilterOptions,
|
|
@@ -334,6 +350,7 @@ declare module 'musora-content-services' {
|
|
|
334
350
|
getUserWeeklyStats,
|
|
335
351
|
globalConfig,
|
|
336
352
|
initializeService,
|
|
353
|
+
isBucketUrl,
|
|
337
354
|
isContentLiked,
|
|
338
355
|
jumpToContinueContent,
|
|
339
356
|
likeComment,
|
|
@@ -378,6 +395,7 @@ declare module 'musora-content-services' {
|
|
|
378
395
|
updatePlaylist,
|
|
379
396
|
updatePlaylistItem,
|
|
380
397
|
updateUserPractice,
|
|
398
|
+
verifyImageSRC,
|
|
381
399
|
verifyLocalDataContext,
|
|
382
400
|
}
|
|
383
401
|
}
|
package/src/index.js
CHANGED
|
@@ -55,6 +55,18 @@ import {
|
|
|
55
55
|
fetchAwardsForUser
|
|
56
56
|
} from './services/gamification/awards.js';
|
|
57
57
|
|
|
58
|
+
import {
|
|
59
|
+
applyCloudflareWrapper,
|
|
60
|
+
applySanityTransformations,
|
|
61
|
+
buildImageSRC
|
|
62
|
+
} from './services/imageSRCBuilder.js';
|
|
63
|
+
|
|
64
|
+
import {
|
|
65
|
+
extractSanityUrl,
|
|
66
|
+
isBucketUrl,
|
|
67
|
+
verifyImageSRC
|
|
68
|
+
} from './services/imageSRCVerify.js';
|
|
69
|
+
|
|
58
70
|
import {
|
|
59
71
|
assignModeratorToComment,
|
|
60
72
|
closeComment,
|
|
@@ -213,10 +225,13 @@ import {
|
|
|
213
225
|
|
|
214
226
|
export {
|
|
215
227
|
addItemToPlaylist,
|
|
228
|
+
applyCloudflareWrapper,
|
|
229
|
+
applySanityTransformations,
|
|
216
230
|
assignModeratorToComment,
|
|
217
231
|
assignmentStatusCompleted,
|
|
218
232
|
assignmentStatusReset,
|
|
219
233
|
blockUser,
|
|
234
|
+
buildImageSRC,
|
|
220
235
|
closeComment,
|
|
221
236
|
contentStatusCompleted,
|
|
222
237
|
contentStatusReset,
|
|
@@ -230,6 +245,7 @@ export {
|
|
|
230
245
|
deletePracticeSession,
|
|
231
246
|
duplicatePlaylist,
|
|
232
247
|
editComment,
|
|
248
|
+
extractSanityUrl,
|
|
233
249
|
fetchAll,
|
|
234
250
|
fetchAllCompletedStates,
|
|
235
251
|
fetchAllFilterOptions,
|
|
@@ -333,6 +349,7 @@ export {
|
|
|
333
349
|
getUserWeeklyStats,
|
|
334
350
|
globalConfig,
|
|
335
351
|
initializeService,
|
|
352
|
+
isBucketUrl,
|
|
336
353
|
isContentLiked,
|
|
337
354
|
jumpToContinueContent,
|
|
338
355
|
likeComment,
|
|
@@ -377,5 +394,6 @@ export {
|
|
|
377
394
|
updatePlaylist,
|
|
378
395
|
updatePlaylistItem,
|
|
379
396
|
updateUserPractice,
|
|
397
|
+
verifyImageSRC,
|
|
380
398
|
verifyLocalDataContext,
|
|
381
399
|
};
|
package/src/services/content.js
CHANGED
|
File without changes
|
package/src/services/forum.js
CHANGED
|
@@ -10,7 +10,8 @@ export async function getActiveDiscussions(brand, { page = 1, limit = 10 } = {})
|
|
|
10
10
|
{
|
|
11
11
|
id: 11,
|
|
12
12
|
url: 'https://forum.example.com/post/11',
|
|
13
|
-
|
|
13
|
+
title: 'My Journey with Pianote',
|
|
14
|
+
post: "Hey everyone, I thought I'd create this topic to detail my journey with Pianote. I started playing piano for the first time on Friday the 10th of March",
|
|
14
15
|
author: {
|
|
15
16
|
id: 123,
|
|
16
17
|
name: 'John Doe',
|
|
@@ -20,7 +21,8 @@ export async function getActiveDiscussions(brand, { page = 1, limit = 10 } = {})
|
|
|
20
21
|
{
|
|
21
22
|
id: 12,
|
|
22
23
|
url: 'https://forum.example.com/post/12',
|
|
23
|
-
|
|
24
|
+
title: 'Learning the Piano',
|
|
25
|
+
post: "I can't wait to share the ups and downs of learning the piano!",
|
|
24
26
|
author: {
|
|
25
27
|
id: 124,
|
|
26
28
|
name: 'Jane Smith',
|
|
@@ -30,7 +32,8 @@ export async function getActiveDiscussions(brand, { page = 1, limit = 10 } = {})
|
|
|
30
32
|
{
|
|
31
33
|
id: 13,
|
|
32
34
|
url: 'https://forum.example.com/post/13',
|
|
33
|
-
|
|
35
|
+
title: 'Piano Inspirations',
|
|
36
|
+
post: "One video that got me so psyched to learn the piano was this guy on YouTube called chocotiger who played George winstons - variations on pachabel.",
|
|
34
37
|
author: {
|
|
35
38
|
id: 125,
|
|
36
39
|
name: 'Alice Johnson',
|
|
@@ -40,7 +43,8 @@ export async function getActiveDiscussions(brand, { page = 1, limit = 10 } = {})
|
|
|
40
43
|
{
|
|
41
44
|
id: 14,
|
|
42
45
|
url: 'https://forum.example.com/post/14',
|
|
43
|
-
|
|
46
|
+
title: 'Feedback on Lessons',
|
|
47
|
+
post: "I appreciate the way the lessons are structured and how fun they are. To newbies like me (and maybe some others), would be great if there was a plug-in (like the one from flowkey) to plug your piano/keyboard and get realtime feedback. I'm also trying to learn how to read the sheet music, and it is sometimes hard to read what key is being played, so having the plug-in would help provide feedback asap instead of rewinding the video, (the fonts on the video are too small)",
|
|
44
48
|
author: {
|
|
45
49
|
id: 126,
|
|
46
50
|
name: 'Bob Williams',
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds an optimized image URL using both Sanity and Cloudflare Image Resizing services
|
|
3
|
+
*
|
|
4
|
+
* This function takes a Sanity CDN URL and image transformation parameters, then:
|
|
5
|
+
* 1. Adds appropriate transformation parameters to the Sanity URL
|
|
6
|
+
* 2. Wraps the resulting URL with Cloudflare's Image Resizing service
|
|
7
|
+
* 3. Maps compatible parameters between the two services for optimal results
|
|
8
|
+
*
|
|
9
|
+
* @param {string} url - The original image URL, typically from Sanity CDN
|
|
10
|
+
* @param {Object} options - Image transformation options
|
|
11
|
+
* @param {number} [options.width] - Desired image width in pixels
|
|
12
|
+
* @param {number} [options.height] - Desired image height in pixels
|
|
13
|
+
* @param {number} [options.quality] - Image quality (1-100)
|
|
14
|
+
* @param {string} [options.fit] - Resize strategy: 'cover', 'contain', 'scale-down', 'none'
|
|
15
|
+
* @param {string} [options.gravity] - Content positioning: 'auto', 'top', 'bottom', 'left', 'right', etc.
|
|
16
|
+
* @param {string} [options.crop] - Sanity-specific crop mode
|
|
17
|
+
* @param {string} [options.format] - Image format: 'auto', 'webp', 'jpeg', 'png', etc.
|
|
18
|
+
* @param {number} [options.blur] - Blur amount
|
|
19
|
+
* @param {number} [options.sharpen] - Sharpen amount
|
|
20
|
+
* @param {number} [options.saturation] - Saturation adjustment
|
|
21
|
+
* @param {number} [options.brightness] - Brightness adjustment
|
|
22
|
+
* @param {number} [options.contrast] - Contrast adjustment
|
|
23
|
+
* @param {number} [options.gamma] - Gamma adjustment
|
|
24
|
+
* @param {number} [options.rotate] - Rotation angle in degrees
|
|
25
|
+
* @param {number} [options.dpr] - Device pixel ratio for responsive images
|
|
26
|
+
* @param {string} [options.background] - Background color (e.g., '#FFFFFF')
|
|
27
|
+
* @param {number} [options.padding] - Padding to add around the image
|
|
28
|
+
* @param {boolean} [options.anim] - Whether to preserve animation in GIFs
|
|
29
|
+
* @param {string} [options.onerror] - Error handling strategy
|
|
30
|
+
* @param {string} [options.compression] - Compression strategy
|
|
31
|
+
* @param {string} [options.fetchFormat] - Format to request from origin
|
|
32
|
+
* @param {string} [options.sampling] - Chroma subsampling strategy
|
|
33
|
+
* @param {string} [options.metadata] - Metadata to preserve
|
|
34
|
+
*
|
|
35
|
+
* @returns {string} The fully constructed image URL with transformations
|
|
36
|
+
*/
|
|
37
|
+
export function buildImageSRC(url, options = {}) {
|
|
38
|
+
// Process Sanity URL first if applicable
|
|
39
|
+
if (url.includes('cdn.sanity.io')) {
|
|
40
|
+
url = applySanityTransformations(url, options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Then apply Cloudflare transformations
|
|
44
|
+
return applyCloudflareWrapper(url, options)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Applies Sanity-specific image transformations to a Sanity CDN URL
|
|
49
|
+
*
|
|
50
|
+
* @param {string} url - The Sanity CDN URL
|
|
51
|
+
* @param {Object} options - Image transformation options
|
|
52
|
+
* @returns {string} URL with Sanity transformations applied
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
export function applySanityTransformations(url, options) {
|
|
56
|
+
const { width, height, quality } = options
|
|
57
|
+
|
|
58
|
+
const sanityOptions = []
|
|
59
|
+
|
|
60
|
+
// Dimensions
|
|
61
|
+
if (width) sanityOptions.push(`w=${width}`)
|
|
62
|
+
if (height) sanityOptions.push(`h=${height}`)
|
|
63
|
+
if (quality) sanityOptions.push(`q=${quality}`)
|
|
64
|
+
|
|
65
|
+
// Add parameters to Sanity URL
|
|
66
|
+
const sanityQuery = sanityOptions.length > 0 ? `?${sanityOptions.join('&')}` : ''
|
|
67
|
+
return `${url}${sanityQuery}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wraps a URL with Cloudflare's Image Resizing service and applies transformations
|
|
72
|
+
*
|
|
73
|
+
* @param {string} url - The source URL (can be any image URL)
|
|
74
|
+
* @param {Object} options - Image transformation options
|
|
75
|
+
* @returns {string} URL with Cloudflare transformations applied
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
export function applyCloudflareWrapper(url, options) {
|
|
79
|
+
const {
|
|
80
|
+
width,
|
|
81
|
+
height,
|
|
82
|
+
quality,
|
|
83
|
+
fit,
|
|
84
|
+
gravity,
|
|
85
|
+
format,
|
|
86
|
+
blur,
|
|
87
|
+
sharpen,
|
|
88
|
+
brightness,
|
|
89
|
+
contrast,
|
|
90
|
+
gamma,
|
|
91
|
+
rotate,
|
|
92
|
+
dpr,
|
|
93
|
+
background,
|
|
94
|
+
padding,
|
|
95
|
+
anim,
|
|
96
|
+
onerror,
|
|
97
|
+
compression,
|
|
98
|
+
fetchFormat,
|
|
99
|
+
sampling,
|
|
100
|
+
metadata,
|
|
101
|
+
} = options
|
|
102
|
+
|
|
103
|
+
const cloudflareOptions = []
|
|
104
|
+
|
|
105
|
+
// Build Cloudflare options - required parameters
|
|
106
|
+
if (width) cloudflareOptions.push(`width=${width}`)
|
|
107
|
+
if (height) cloudflareOptions.push(`height=${height}`)
|
|
108
|
+
if (quality) cloudflareOptions.push(`quality=${quality}`)
|
|
109
|
+
|
|
110
|
+
// Add optional Cloudflare parameters
|
|
111
|
+
if (format) cloudflareOptions.push(`format=${format}`)
|
|
112
|
+
if (fit) cloudflareOptions.push(`fit=${fit}`)
|
|
113
|
+
if (gravity) cloudflareOptions.push(`gravity=${gravity}`)
|
|
114
|
+
|
|
115
|
+
if (sharpen !== null && sharpen !== undefined) cloudflareOptions.push(`sharpen=${sharpen}`)
|
|
116
|
+
if (blur !== null && blur !== undefined) cloudflareOptions.push(`blur=${blur}`)
|
|
117
|
+
if (brightness !== null && brightness !== undefined)
|
|
118
|
+
cloudflareOptions.push(`brightness=${brightness}`)
|
|
119
|
+
if (contrast !== null && contrast !== undefined) cloudflareOptions.push(`contrast=${contrast}`)
|
|
120
|
+
if (gamma !== null && gamma !== undefined) cloudflareOptions.push(`gamma=${gamma}`)
|
|
121
|
+
if (rotate !== null && rotate !== undefined) cloudflareOptions.push(`rotate=${rotate}`)
|
|
122
|
+
if (dpr !== null && dpr !== undefined) cloudflareOptions.push(`dpr=${dpr}`)
|
|
123
|
+
|
|
124
|
+
if (metadata !== null && metadata !== undefined) cloudflareOptions.push(`metadata=${metadata}`)
|
|
125
|
+
if (onerror !== null && onerror !== undefined) cloudflareOptions.push(`onerror=${onerror}`)
|
|
126
|
+
if (anim === false) cloudflareOptions.push('anim=false')
|
|
127
|
+
if (background !== null && background !== undefined)
|
|
128
|
+
cloudflareOptions.push(`background=${background}`)
|
|
129
|
+
if (fetchFormat !== null && fetchFormat !== undefined)
|
|
130
|
+
cloudflareOptions.push(`fetchFormat=${fetchFormat}`)
|
|
131
|
+
if (compression !== null && compression !== undefined)
|
|
132
|
+
cloudflareOptions.push(`compression=${compression}`)
|
|
133
|
+
if (sampling !== null && sampling !== undefined) cloudflareOptions.push(`sampling=${sampling}`)
|
|
134
|
+
if (padding !== null && padding !== undefined) cloudflareOptions.push(`padding=${padding}`)
|
|
135
|
+
|
|
136
|
+
const optionsString = cloudflareOptions.length > 0 ? cloudflareOptions.join(',') : ''
|
|
137
|
+
|
|
138
|
+
return `https://www.musora.com/cdn-cgi/image/${optionsString}/${url}`
|
|
139
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies if an image URL follows best practices for optimization
|
|
3
|
+
*
|
|
4
|
+
* This function checks whether:
|
|
5
|
+
* 1. Sanity CDN images include query parameters for optimization
|
|
6
|
+
* 2. Direct S3 images are avoided (should use CDN instead)
|
|
7
|
+
*
|
|
8
|
+
* @param {string} src - The image source URL to verify
|
|
9
|
+
* @returns {void}
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Check a direct Sanity URL
|
|
13
|
+
* verifyImageSRC('https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg');
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Check a Sanity URL inside a Cloudflare URL
|
|
17
|
+
* verifyImageSRC('https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg');
|
|
18
|
+
*/
|
|
19
|
+
export function verifyImageSRC(src) {
|
|
20
|
+
// Exit early if the URL is empty
|
|
21
|
+
if (!src) return
|
|
22
|
+
|
|
23
|
+
// Check for S3 direct URLs
|
|
24
|
+
if (isBucketUrl(src)) {
|
|
25
|
+
warnAboutDirectS3Url(src)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check for Sanity URLs
|
|
30
|
+
if (src.includes('cdn.sanity.io')) {
|
|
31
|
+
verifySanityUrl(src)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a URL is a direct link to an S3 bucket
|
|
37
|
+
*
|
|
38
|
+
* @param {string} url - The URL to check
|
|
39
|
+
* @returns {boolean} True if the URL is a direct S3 bucket URL
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
export function isBucketUrl(url) {
|
|
43
|
+
// Check for common S3 patterns
|
|
44
|
+
return (
|
|
45
|
+
url.includes('.s3.amazonaws.com') ||
|
|
46
|
+
url.includes('s3.us-') ||
|
|
47
|
+
url.includes('amazonaws.com/') ||
|
|
48
|
+
(url.includes('musora-') && url.includes('.s3.'))
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Issues a warning about using direct S3 URLs instead of a CDN
|
|
54
|
+
*
|
|
55
|
+
* @param {string} url - The S3 URL that triggered the warning
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
function warnAboutDirectS3Url(url) {
|
|
59
|
+
// Only warn in development mode
|
|
60
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
61
|
+
console.warn(`WARNING: Direct S3 bucket URL detected: ${url}
|
|
62
|
+
This is not recommended. Use Cloudfront or another CDN for better performance.`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extracts a Sanity URL from a potentially Cloudflare-wrapped URL
|
|
68
|
+
*
|
|
69
|
+
* @param {string} url - The URL to process
|
|
70
|
+
* @returns {string} The extracted Sanity URL
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
export function extractSanityUrl(url) {
|
|
74
|
+
// If this is a Cloudflare URL, extract the Sanity portion
|
|
75
|
+
if (url.includes('/cdn-cgi/image/')) {
|
|
76
|
+
// Split the URL to get the portion after /cdn-cgi/image/[parameters]/
|
|
77
|
+
const parts = url.split('/cdn-cgi/image/')
|
|
78
|
+
if (parts.length > 1) {
|
|
79
|
+
// Get everything after the first / that follows the Cloudflare parameters
|
|
80
|
+
const cfParamsAndSanityUrl = parts[1]
|
|
81
|
+
const slashIndex = cfParamsAndSanityUrl.indexOf('/', 0)
|
|
82
|
+
if (slashIndex !== -1) {
|
|
83
|
+
let sanityUrl = cfParamsAndSanityUrl.substring(slashIndex + 1)
|
|
84
|
+
return sanityUrl
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If not a Cloudflare URL or extraction failed, return the original URL
|
|
90
|
+
return url
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Verifies if a Sanity CDN image URL includes optimization parameters
|
|
95
|
+
*
|
|
96
|
+
* @param {string} src - The Sanity image URL to verify
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
function verifySanityUrl(src) {
|
|
100
|
+
// Extract the Sanity URL if it's wrapped in a Cloudflare URL
|
|
101
|
+
const sanityUrl = extractSanityUrl(src)
|
|
102
|
+
|
|
103
|
+
// Check if the Sanity URL has parameters (any query string)
|
|
104
|
+
const hasParameters = sanityUrl.includes('?')
|
|
105
|
+
|
|
106
|
+
// Warn if no parameters are found
|
|
107
|
+
if (!hasParameters) {
|
|
108
|
+
// Only warn in development mode
|
|
109
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
110
|
+
console.warn(`WARNING: Sanity CDN URL without parameters detected: ${src}
|
|
111
|
+
This may cause performance issues. Consider adding image transformations using the buildImageSRC MCS service.`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -222,7 +222,7 @@ export async function fetchCompletedContent(type = 'all', brand, { page, limit }
|
|
|
222
222
|
* .catch(error => console.error(error));
|
|
223
223
|
*/
|
|
224
224
|
export async function fetchContentPageUserData(contentId) {
|
|
225
|
-
let url = `/content/${contentId}/user_data/${globalConfig.railcontentConfig.userId}`
|
|
225
|
+
let url = `/api/content/v1/${contentId}/user_data/${globalConfig.railcontentConfig.userId}`
|
|
226
226
|
const headers = {
|
|
227
227
|
'Content-Type': 'application/json',
|
|
228
228
|
Accept: 'application/json',
|
|
@@ -873,8 +873,8 @@ export async function postContentComplete(contentId) {
|
|
|
873
873
|
}
|
|
874
874
|
|
|
875
875
|
export async function postContentReset(contentId) {
|
|
876
|
-
let url = `/content/user/progress/reset`
|
|
877
|
-
return postDataHandler(url
|
|
876
|
+
let url = `/api/content/v1/user/progress/reset/${contentId}`
|
|
877
|
+
return postDataHandler(url)
|
|
878
878
|
}
|
|
879
879
|
|
|
880
880
|
/**
|
|
@@ -7,15 +7,13 @@ import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
|
7
7
|
import {fetchByRailContentIds} from "./sanity";
|
|
8
8
|
import {lessonTypesMapping} from "../contentTypeConfig";
|
|
9
9
|
|
|
10
|
-
const recentActivity =
|
|
11
|
-
|
|
12
|
-
{ id:
|
|
13
|
-
{ id:
|
|
14
|
-
{ id:
|
|
15
|
-
{ id:
|
|
16
|
-
|
|
17
|
-
],
|
|
18
|
-
}
|
|
10
|
+
const recentActivity = [
|
|
11
|
+
{ id: 5,title: '3 Easy Classical Songs For Beginners', action: 'Comment', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/8a7fb4d7473306c5fa51ba2e8867e03d44342b18-1920x1080.jpg', summary: 'Just completed the advanced groove lesson! I’m finally feeling more confident with my fills. Thanks for the clear explanations and practice tips! ', date: '2025-03-25 10:09:48' },
|
|
12
|
+
{ id:4, title: 'Piano Man by Billy Joel', action: 'Play', thumbnail:'https://cdn.sanity.io/images/4032r8py/production/107c258114540170399dfd72a50dae51575552f4-1000x1000.jpg', date: '2025-03-25 10:04:48' },
|
|
13
|
+
{ id:3, title: 'General Piano Discussion', action: 'Post', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/2331571d237b42dbf72f0cf35fdf163d996c5c5a-1920x1080.jpg', summary: 'Just completed the advanced groove lesson! I’m finally feeling more confident with my fills. Thanks for the clear explanations and practice tips! ', date: '2025-03-25 09:49:48' },
|
|
14
|
+
{ id:2, title: 'Welcome To Guitareo', action: 'Complete', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/2331571d237b42dbf72f0cf35fdf163d996c5c5a-1920x1080.jpg',date: '2025-03-25 09:34:48' },
|
|
15
|
+
{ id:1, title: 'Welcome To Guitareo', action: 'Start', thumbnail: 'https://cdn.sanity.io/images/4032r8py/production/2331571d237b42dbf72f0cf35fdf163d996c5c5a-1920x1080.jpg',date: '2025-03-25 09:04:48' },
|
|
16
|
+
]
|
|
19
17
|
|
|
20
18
|
const DATA_KEY_PRACTICES = 'practices'
|
|
21
19
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { buildImageSRC, applySanityTransformations } = require('../src/services/imageSRCBuilder.js')
|
|
2
|
+
|
|
3
|
+
describe('imageSRCBuilder', function () {
|
|
4
|
+
beforeEach(() => {})
|
|
5
|
+
|
|
6
|
+
test('applySanityTransformations', async () => {
|
|
7
|
+
const url =
|
|
8
|
+
'https://cdn.sanity.io/images/4032r8py/production/83c3b6e7354a46c605804c093f707daa4e3f8f25-8000x4500.png'
|
|
9
|
+
const width = 500
|
|
10
|
+
const height = 100
|
|
11
|
+
const quality = 95
|
|
12
|
+
const resultingURL = applySanityTransformations(url, {
|
|
13
|
+
width: width,
|
|
14
|
+
quality: quality,
|
|
15
|
+
height: height,
|
|
16
|
+
})
|
|
17
|
+
const expected = `${url}?w=${width}&h=${height}&q=${quality}`
|
|
18
|
+
|
|
19
|
+
expect(resultingURL).toStrictEqual(expected)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('buildImageSRC', async () => {
|
|
23
|
+
const url = 'https://d2vyvo0tyx8ig5.cloudfront.net/books/foundations/level-2.jpg'
|
|
24
|
+
const width = 500
|
|
25
|
+
const height = 100
|
|
26
|
+
const quality = 95
|
|
27
|
+
const resultingURL = buildImageSRC(url, {
|
|
28
|
+
width: width,
|
|
29
|
+
quality: quality,
|
|
30
|
+
height: height,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const expected = `https://www.musora.com/cdn-cgi/image/width=${width},height=${height},quality=${quality}/${url}`
|
|
34
|
+
|
|
35
|
+
expect(resultingURL).toStrictEqual(expected)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Mock console.warn to avoid cluttering test output and to verify warnings
|
|
2
|
+
import { extractSanityUrl, isBucketUrl, verifyImageSRC } from '../src/services/imageSRCVerify.js'
|
|
3
|
+
|
|
4
|
+
const originalConsoleWarn = console.warn
|
|
5
|
+
const originalConsoleError = console.error
|
|
6
|
+
const originalNodeEnv = process.env.NODE_ENV
|
|
7
|
+
|
|
8
|
+
describe('Image URL Verification', () => {
|
|
9
|
+
let consoleWarnMock
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Mock console.warn and console.error
|
|
13
|
+
consoleWarnMock = jest.fn()
|
|
14
|
+
console.warn = consoleWarnMock
|
|
15
|
+
console.error = jest.fn()
|
|
16
|
+
|
|
17
|
+
// Set NODE_ENV to development for all tests
|
|
18
|
+
process.env.NODE_ENV = 'development'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
// Restore the original console methods and NODE_ENV
|
|
23
|
+
console.warn = originalConsoleWarn
|
|
24
|
+
console.error = originalConsoleError
|
|
25
|
+
process.env.NODE_ENV = originalNodeEnv
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('verifyImageSRC', () => {
|
|
29
|
+
test('should not warn for Sanity URL with parameters', () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const url =
|
|
32
|
+
'https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg?w=500&q=95'
|
|
33
|
+
|
|
34
|
+
// Act
|
|
35
|
+
verifyImageSRC(url)
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(consoleWarnMock).not.toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should warn for Sanity URL without parameters', () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
const url =
|
|
44
|
+
'https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg'
|
|
45
|
+
|
|
46
|
+
// Act
|
|
47
|
+
verifyImageSRC(url)
|
|
48
|
+
|
|
49
|
+
// Assert
|
|
50
|
+
expect(consoleWarnMock).toHaveBeenCalled()
|
|
51
|
+
expect(consoleWarnMock.mock.calls[0][0]).toContain(
|
|
52
|
+
'WARNING: Sanity CDN URL without parameters detected'
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('should warn for direct S3 bucket URL', () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const url = 'https://musora-images.s3.amazonaws.com/drumeo/images/some-image.jpg'
|
|
59
|
+
|
|
60
|
+
// Act
|
|
61
|
+
verifyImageSRC(url)
|
|
62
|
+
|
|
63
|
+
// Assert
|
|
64
|
+
expect(consoleWarnMock).toHaveBeenCalled()
|
|
65
|
+
expect(consoleWarnMock.mock.calls[0][0]).toContain('Direct S3 bucket URL detected')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should not warn when URL is empty', () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
const url = ''
|
|
71
|
+
|
|
72
|
+
// Act
|
|
73
|
+
verifyImageSRC(url)
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
76
|
+
expect(consoleWarnMock).not.toHaveBeenCalled()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('Cloudflare wrapped Sanity URLs', () => {
|
|
81
|
+
test('should extract and validate Sanity URL from Cloudflare wrapper', () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const url =
|
|
84
|
+
'https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg'
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
verifyImageSRC(url)
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
expect(consoleWarnMock).toHaveBeenCalled()
|
|
91
|
+
expect(consoleWarnMock.mock.calls[0][0]).toContain(
|
|
92
|
+
'WARNING: Sanity CDN URL without parameters detected'
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('should not warn for Cloudflare wrapped Sanity URL with parameters', () => {
|
|
97
|
+
// Arrange
|
|
98
|
+
const url =
|
|
99
|
+
'https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/4032r8py/staging/504c4e3393170f937a579de6f3c75c457b0c9e65-640x360.jpg?w=500&q=95'
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
verifyImageSRC(url)
|
|
103
|
+
|
|
104
|
+
// Assert
|
|
105
|
+
expect(consoleWarnMock).not.toHaveBeenCalled()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('isBucketUrl', () => {
|
|
110
|
+
test('should detect standard S3 URLs', () => {
|
|
111
|
+
// Arrange & Act & Assert
|
|
112
|
+
expect(isBucketUrl('https://my-bucket.s3.amazonaws.com/image.jpg')).toBe(true)
|
|
113
|
+
expect(isBucketUrl('https://s3.us-east-1.amazonaws.com/my-bucket/image.jpg')).toBe(true)
|
|
114
|
+
expect(isBucketUrl('https://musora-images.s3.amazonaws.com/path/to/image.jpg')).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('should not flag non-S3 URLs', () => {
|
|
118
|
+
// Arrange & Act & Assert
|
|
119
|
+
expect(isBucketUrl('https://cdn.sanity.io/images/123/production/image.jpg')).toBe(false)
|
|
120
|
+
expect(isBucketUrl('https://www.musora.com/image.jpg')).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('extractSanityUrl', () => {
|
|
125
|
+
test('should extract Sanity URL from Cloudflare wrapper', () => {
|
|
126
|
+
// Arrange
|
|
127
|
+
const wrappedUrl =
|
|
128
|
+
'https://www.musora.com/cdn-cgi/image/width=500,quality=95/https://cdn.sanity.io/images/123/production/image.jpg'
|
|
129
|
+
const expectedExtracted = 'https://cdn.sanity.io/images/123/production/image.jpg'
|
|
130
|
+
|
|
131
|
+
// Act
|
|
132
|
+
const result = extractSanityUrl(wrappedUrl)
|
|
133
|
+
|
|
134
|
+
// Assert
|
|
135
|
+
expect(result).toBe(expectedExtracted)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('should return original URL if not Cloudflare wrapped', () => {
|
|
139
|
+
// Arrange
|
|
140
|
+
const url = 'https://cdn.sanity.io/images/123/production/image.jpg'
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
const result = extractSanityUrl(url)
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
expect(result).toBe(url)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('should handle malformed Cloudflare URLs gracefully', () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const malformedUrl = 'https://www.musora.com/cdn-cgi/image/width=500'
|
|
152
|
+
|
|
153
|
+
// Act
|
|
154
|
+
const result = extractSanityUrl(malformedUrl)
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
157
|
+
expect(result).toBe(malformedUrl)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|