musora-content-services 1.0.7

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +39 -0
  3. package/docs/data/search.json +1 -0
  4. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  5. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  6. package/docs/fonts/OpenSans-Bold-webfont.svg +1830 -0
  7. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  8. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  9. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  10. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  11. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  12. package/docs/fonts/OpenSans-Italic-webfont.svg +1830 -0
  13. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  14. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  15. package/docs/fonts/OpenSans-Light-webfont.svg +1831 -0
  16. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  17. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  18. package/docs/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  19. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  20. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  21. package/docs/fonts/OpenSans-Regular-webfont.svg +1831 -0
  22. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  23. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  24. package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
  25. package/docs/fonts/OpenSans-Semibold-webfont.svg +1830 -0
  26. package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  27. package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
  28. package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  29. package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +1830 -0
  30. package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  31. package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  32. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  33. package/docs/global.html +4773 -0
  34. package/docs/index.html +93 -0
  35. package/docs/index.js.html +766 -0
  36. package/docs/scripts/core.js +726 -0
  37. package/docs/scripts/core.min.js +23 -0
  38. package/docs/scripts/linenumber.js +25 -0
  39. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  40. package/docs/scripts/prettify/lang-css.js +2 -0
  41. package/docs/scripts/prettify/prettify.js +28 -0
  42. package/docs/scripts/resize.js +90 -0
  43. package/docs/scripts/search.js +265 -0
  44. package/docs/scripts/search.min.js +6 -0
  45. package/docs/scripts/third-party/Apache-License-2.0.txt +202 -0
  46. package/docs/scripts/third-party/fuse.js +9 -0
  47. package/docs/scripts/third-party/hljs-line-num-original.js +369 -0
  48. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  49. package/docs/scripts/third-party/hljs-original.js +5171 -0
  50. package/docs/scripts/third-party/hljs.js +1 -0
  51. package/docs/scripts/third-party/popper.js +5 -0
  52. package/docs/scripts/third-party/tippy.js +1 -0
  53. package/docs/scripts/third-party/tocbot.js +672 -0
  54. package/docs/scripts/third-party/tocbot.min.js +1 -0
  55. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  56. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  57. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  58. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  59. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  60. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  61. package/docs/styles/jsdoc-default.css +692 -0
  62. package/docs/styles/prettify-jsdoc.css +111 -0
  63. package/docs/styles/prettify-tomorrow.css +132 -0
  64. package/jsdoc.json +13 -0
  65. package/package.json +27 -0
  66. package/publish.sh +21 -0
  67. package/src/index.js +758 -0
package/src/index.js ADDED
@@ -0,0 +1,758 @@
1
+ let globalConfig = {};
2
+
3
+ /**
4
+ * Initializes the Sanity service with the given configuration.
5
+ * This function must be called before using any other functions in this library.
6
+ *
7
+ * @param {Object} config - Configuration object containing Sanity API settings.
8
+ * @param {string} config.token - The API token for authenticating with Sanity.
9
+ * @param {string} config.projectId - The project ID in Sanity.
10
+ * @param {string} config.dataset - The dataset name in Sanity.
11
+ * @param {string} config.version - The API version to use.
12
+ * @param {boolean} [config.debug=false] - Optional flag to enable debug mode, which logs the query and results.
13
+ *
14
+ * @example
15
+ * // Initialize the Sanity service in your app.js
16
+ * initializeSanityService({
17
+ * token: 'your-sanity-api-token',
18
+ * projectId: 'your-sanity-project-id',
19
+ * dataset: 'your-dataset-name',
20
+ * version: '2021-06-07',
21
+ * debug: true // Optional: Enable debug mode
22
+ * });
23
+ */
24
+ function initializeSanityService(config) {
25
+ globalConfig = config;
26
+ }
27
+
28
+ /**
29
+ * Fetch a song by its document ID from Sanity.
30
+ * @param {string} documentId - The ID of the document to fetch.
31
+ * @returns {Promise<Object|null>} - The fetched song data or null if not found.
32
+ */
33
+ async function fetchSongById(documentId) {
34
+ const fields = [
35
+ 'title',
36
+ '"thumbnail_url": thumbnail.asset->url',
37
+ '"style": genre[0]->name',
38
+ '"artist": artist->name',
39
+ 'album',
40
+ 'instrumentless',
41
+ 'soundslice',
42
+ '"resources": resource[]{resource_url, resource_name}',
43
+ ];
44
+
45
+ const query = `
46
+ *[_type == "song" && railcontent_id == ${documentId}]{
47
+ ${fields.join(', ')}
48
+ }`;
49
+ return fetchSanity(query);
50
+ }
51
+
52
+ /**
53
+ * Fetch all artists with lessons available for a specific brand.
54
+ * @param {string} brand - The brand for which to fetch artists.
55
+ * @returns {Promise<Object|null>} - The fetched artist data or null if not found.
56
+ */
57
+ async function fetchArtists(brand) {
58
+ const query = `
59
+ *[_type == "artist"]{
60
+ name,
61
+ "lessonsCount": count(*[_type == "song" && brand == "${brand}" && references(^._id)])
62
+ }[lessonsCount > 0]`;
63
+ return fetchSanity(query, true);
64
+ }
65
+
66
+ /**
67
+ * Fetch related songs for a specific brand and song ID.
68
+ * @param {string} brand - The brand for which to fetch related songs.
69
+ * @param {string} songId - The ID of the song to find related songs for.
70
+ * @returns {Promise<Object|null>} - The fetched related songs data or null if not found.
71
+ */
72
+ async function fetchRelatedSongs(brand, songId) {
73
+ const query = `
74
+ *[_type == "song" && railcontent_id == ${songId}]{
75
+ "data": array::unique([
76
+ ...(*[_type == "song" && brand == "${brand}" && railcontent_id != ${songId} && references(^.artist->_id)]{
77
+ "type": _type,
78
+ "id": railcontent_id,
79
+ "url": web_url_path,
80
+ "published_on": published_on,
81
+ status,
82
+ "fields": [
83
+ {
84
+ "key": "title",
85
+ "value": title
86
+ },
87
+ {
88
+ "key": "artist",
89
+ "value": artist->name
90
+ },
91
+ {
92
+ "key": "difficulty",
93
+ "value": difficulty
94
+ },
95
+ {
96
+ "key": "length_in_seconds",
97
+ "value": soundslice[0].soundslice_length_in_second
98
+ }
99
+ ],
100
+ "data": [{
101
+ "key": "thumbnail_url",
102
+ "value": thumbnail.asset->url
103
+ }]
104
+ }[0...10]),
105
+ ...(*[_type == "song" && brand == "${brand}" && railcontent_id != ${songId} && references(^.genre[]->_id)]{
106
+ "type": _type,
107
+ "id": railcontent_id,
108
+ "url": web_url_path,
109
+ "published_on": published_on,
110
+ status,
111
+ "fields": [
112
+ {
113
+ "key": "title",
114
+ "value": title
115
+ },
116
+ {
117
+ "key": "artist",
118
+ "value": artist->name
119
+ },
120
+ {
121
+ "key": "difficulty",
122
+ "value": difficulty
123
+ },
124
+ {
125
+ "key": "length_in_seconds",
126
+ "value": soundslice[0].soundslice_length_in_second
127
+ }
128
+ ],
129
+ "data": [{
130
+ "key": "thumbnail_url",
131
+ "value": thumbnail.asset->url
132
+ }]
133
+ }[0...10])
134
+ ])[0...10]
135
+ }`;
136
+
137
+ return fetchSanity(query);
138
+ }
139
+
140
+ /**
141
+ * Fetch all songs for a specific brand with pagination and search options.
142
+ * @param {string} brand - The brand for which to fetch songs.
143
+ * @param {Object} params - Parameters for pagination, filtering, and sorting.
144
+ * @param {number} [params.page=1] - The page number for pagination.
145
+ * @param {number} [params.limit=10] - The number of songs per page.
146
+ * @param {string} [params.searchTerm=""] - The search term to filter songs by title or artist.
147
+ * @param {string} [params.sort="-published_on"] - The field to sort the songs by.
148
+ * @param {Array<string>} [params.includedFields=[]] - The fields to include in the query.
149
+ * @param {string} [params.groupBy=""] - The field to group the results by.
150
+ * @returns {Promise<Object|null>} - The fetched song data or null if not found.
151
+ */
152
+ async function fetchAllSongs(brand, { page = 1, limit = 10, searchTerm = "", sort = "-published_on", includedFields = [] , groupBy = "" }) {
153
+ console.log('groupBy', groupBy)
154
+ const start = (page - 1) * limit;
155
+ const end = start + limit;
156
+
157
+ // Construct the search filter
158
+ const searchFilter = searchTerm
159
+ ? `&& (artist->name match "${searchTerm}*" || title match "${searchTerm}*")`
160
+ : "";
161
+
162
+ // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
163
+ const includedFieldsFilter = includedFields.length > 0
164
+ ? includedFields.map(field => {
165
+ let [key, value] = field.split(',');
166
+ if (key === 'difficulty') {
167
+ key = 'difficulty_string';
168
+ }
169
+ return `&& ${key} == "${value}"`;
170
+ }).join(' ')
171
+ : "";
172
+
173
+ // Determine the sort order
174
+ let sortOrder;
175
+ switch (sort) {
176
+ case "slug":
177
+ sortOrder = "artist->name asc";
178
+ break;
179
+ case "published_on":
180
+ sortOrder = "published_on desc";
181
+ break;
182
+ case "-published_on":
183
+ sortOrder = "published_on asc";
184
+ break;
185
+ case "-slug":
186
+ sortOrder = "artist->name desc";
187
+ break;
188
+ case "-popularity":
189
+ sortOrder = "popularity desc";
190
+ break;
191
+ default:
192
+ sortOrder = "published_on asc";
193
+ break;
194
+ }
195
+
196
+ // Determine the group by clause
197
+ let query = "";
198
+ if (groupBy === "artist") {
199
+ query = `
200
+ {
201
+ "total": count(*[_type == 'artist' && count(*[_type == 'song' && brand == '${brand}' && ^._id == artist._ref ]._id) > 0]),
202
+ "entity": *[_type == 'artist' && count(*[_type == 'song' && brand == '${brand}' && ^._id == artist._ref ]._id) > 0]
203
+ {
204
+ 'id': _id,
205
+ 'type': _type,
206
+ name,
207
+ 'head_shot_picture_url': thumbnail_url.asset->url,
208
+ 'all_lessons_count': count(*[_type == 'song' && brand == '${brand}' && ^._id == artist._ref ]._id),
209
+ 'lessons': *[_type == 'song' && brand == '${brand}' && ^._id == artist._ref ]{
210
+ railcontent_id,
211
+ title,
212
+ "image": thumbnail.asset->url,
213
+ "artist_name": artist->name,
214
+ artist,
215
+ difficulty,
216
+ difficulty_string,
217
+ web_url_path,
218
+ published_on
219
+ }[0...10]
220
+ }
221
+ |order(${sortOrder})
222
+ [${start}...${end}]
223
+ }`;
224
+ } else if (groupBy === "genre") {
225
+ query = `
226
+ {
227
+ "total": count(*[_type == 'genre' && count(*[_type == 'song' && brand == '${brand}' && ^._id in genre[]._ref ]._id) > 0]),
228
+ "entity":
229
+ *[_type == 'genre' && count(*[_type == 'song' && brand == '${brand}' && ^._id in genre[]._ref ]._id)>0]
230
+ {
231
+ 'id': _id,
232
+ 'type': _type,
233
+ name,
234
+ 'head_shot_picture_url': thumbnail_url.asset->url,
235
+ 'all_lessons_count': count(*[_type == 'song' && brand == '${brand}' && ^._id in genre[]._ref ]._id),
236
+ 'lessons': *[_type == 'song' && brand == '${brand}' && ^._id in genre[]._ref ]{
237
+ railcontent_id,
238
+ title,
239
+ "image": thumbnail.asset->url,
240
+ "artist_name": artist->name,
241
+ artist,
242
+ difficulty,
243
+ difficulty_string,
244
+ web_url_path,
245
+ published_on
246
+ }[0...10]
247
+ }
248
+ |order(${sortOrder})
249
+ [${start}...${end}]
250
+ }`;
251
+ } else {
252
+ query = `
253
+ {
254
+ "entity": *[_type == 'song' && brand == "${brand}" ${searchFilter} ${includedFieldsFilter}] | order(${sortOrder}) [${start}...${end}] {
255
+ railcontent_id,
256
+ title,
257
+ "image": thumbnail.asset->url,
258
+ "artist_name": artist->name,
259
+ artist,
260
+ difficulty,
261
+ difficulty_string,
262
+ web_url_path,
263
+ published_on
264
+ },
265
+ "total": count(*[_type == 'song' && brand == "${brand}" ${searchFilter} ${includedFieldsFilter}])
266
+ }
267
+ `;
268
+ }
269
+
270
+ return fetchSanity(query);
271
+ }
272
+
273
+ /**
274
+ * Fetch filter options for a specific brand.
275
+ * @param {string} brand - The brand for which to fetch filter options.
276
+ * @returns {Promise<Object|null>} - The fetched filter options or null if not found.
277
+ */
278
+ async function fetchFilterOptions(brand) {
279
+ const query = `
280
+ {
281
+ "difficulty": [
282
+ {"type": "Introductory", "count": count(*[_type == 'song' && brand == ${brand} && difficulty_string == "Introductory"]._id)},
283
+ {"type": "Beginner", "count": count(*[_type == 'song' && brand == ${brand} && difficulty_string == "Beginner"]._id)},
284
+ {"type": "Intermediate", "count": count(*[_type == 'song' && brand == ${brand} && difficulty_string == "Intermediate"]._id)},
285
+ {"type": "Advanced", "count": count(*[_type == 'song' && brand == ${brand} && difficulty_string == "Advanced"]._id)},
286
+ {"type": "Expert", "count": count(*[_type == 'song' && brand == ${brand} && difficulty_string == "Expert"]._id)}
287
+ ],
288
+ "genre": *[_type == 'genre' && 'song' in filter_types] {
289
+ "type": name,
290
+ "count": count(*[_type == 'song' && brand == ${brand} && references(^._id)]._id)
291
+ },
292
+ "instrumentless": [
293
+ {"type": "Full Song Only", "count": count(*[_type == 'song' && brand == ${brand} && instrumentless == false]._id)},
294
+ {"type": "Instrument Removed", "count": count(*[_type == 'song' && brand == ${brand} && instrumentless == true]._id)}
295
+ ]
296
+ }
297
+ `;
298
+
299
+ return fetchSanity(query);
300
+ }
301
+
302
+ /**
303
+ * Fetch the total count of songs for a specific brand.
304
+ * @param {string} brand - The brand for which to fetch the song count.
305
+ * @returns {Promise<number|null>} - The total count of songs or null if an error occurs.
306
+ */
307
+ async function fetchSongCount(brand) {
308
+ const query = `count(*[_type == 'song' && brand == "${brand}"])`;
309
+ return fetchSanity(query);
310
+ }
311
+
312
+ /**
313
+ * Fetch the latest workouts for the home page of a specific brand.
314
+ * @param {string} brand - The brand for which to fetch workouts.
315
+ * @returns {Promise<Object|null>} - The fetched workout data or null if not found.
316
+ */
317
+ async function fetchWorkouts(brand) {
318
+ const query = `*[_type == 'workout' && brand == '${brand}'] [0...5] {
319
+ railcontent_id,
320
+ title,
321
+ "image": thumbnail.asset->url,
322
+ "artist_name": artist->name,
323
+ artist,
324
+ difficulty,
325
+ difficulty_string,
326
+ web_url_path,
327
+ published_on
328
+ } | order(published_on desc)[0...5]`
329
+ return fetchSanity(query);
330
+ }
331
+
332
+ /**
333
+ * Fetch the latest new releases for a specific brand.
334
+ * @param {string} brand - The brand for which to fetch new releases.
335
+ * @returns {Promise<Object|null>} - The fetched new releases data or null if not found.
336
+ */
337
+ async function fetchNewReleases(brand) {
338
+ const newTypes = {
339
+ 'drumeo': ["drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "question-and-answer", "student-collaborations", "live-streams", "live", "podcasts", "solos", "boot-camps", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
340
+ 'pianote': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
341
+ 'guitareo': ["student-review", "student-reviews", "question-and-answer", "archives", "recording", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
342
+ 'singeo': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"],
343
+ 'default': ["student-review", "student-reviews", "question-and-answer", "course", "play-along", "student-focus", "coach-stream", "learning-path-level", "unit", "quick-tips", "live", "question-and-answer", "student-review", "boot-camps", "song", "chords-and-scales", "pack", "podcasts", "workout", "challenge", "challenge-part"]
344
+ };
345
+ const typesString = arrayJoinWithQuotes(newTypes[brand] ?? newTypes['default']);
346
+ const query = `*[_type in [${typesString}] && brand == '${brand}'] | order(releaseDate desc) [0...5] {
347
+ railcontent_id,
348
+ title,
349
+ "image": thumbnail.asset->url,
350
+ "artist_name": artist->name,
351
+ artist,
352
+ difficulty,
353
+ difficulty_string,
354
+ web_url_path,
355
+ published_on
356
+ } | order(published_on desc)[0...5]`
357
+ return fetchSanity(query);
358
+ }
359
+
360
+ /**
361
+ * Fetch upcoming events for a specific brand.
362
+ * @param {string} brand - The brand for which to fetch upcoming events.
363
+ * @returns {Promise<Object|null>} - The fetched upcoming events data or null if not found.
364
+ */
365
+ async function fetchUpcomingEvents(brand) {
366
+ const liveTypes = {
367
+ 'drumeo': ["drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "question-and-answer", "student-collaborations", "live-streams", "live", "podcasts", "solos", "boot-camps", "gear-guides", "performances", "in-rhythm", "challenges", "on-the-road", "diy-drum-experiments", "rhythmic-adventures-of-captain-carson", "study-the-greats", "rhythms-from-another-planet", "tama-drums", "paiste-cymbals", "behind-the-scenes", "exploring-beats", "sonor-drums", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"],
368
+ 'pianote': ["student-review", "student-reviews", "question-and-answer", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"],
369
+ 'guitareo': ["student-review", "student-reviews", "question-and-answer", "archives", "recording", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"],
370
+ 'singeo': ["student-review", "student-reviews", "question-and-answer", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"],
371
+ 'default': ["student-review", "student-reviews", "question-and-answer", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"]
372
+ };
373
+ const typesString = arrayJoinWithQuotes(liveTypes[brand] ?? liveTypes['default']);
374
+ const now = getSanityDate(new Date());
375
+ //TODO: status = 'scheduled' is this handled in sanity?
376
+ const query = `*[_type in [${typesString}] && brand == '${brand}' && published_on > '${now}']{
377
+ railcontent_id,
378
+ title,
379
+ "image": thumbnail.asset->url,
380
+ "artist_name": artist->name,
381
+ artist,
382
+ difficulty,
383
+ difficulty_string,
384
+ web_url_path,
385
+ published_on
386
+ } | order(published_on asc)[0...5]`;
387
+ return fetchSanity(query);
388
+ }
389
+
390
+ /**
391
+ * Fetch content by a specific Railcontent ID.
392
+ * @param {string} id - The Railcontent ID of the content to fetch.
393
+ * @returns {Promise<Object|null>} - The fetched content data or null if not found.
394
+ */
395
+ async function fetchByRailContentId(id) {
396
+ const query = `*[railcontent_id = ${id}]{
397
+ railcontent_id,
398
+ title,
399
+ "image": thumbnail.asset->url,
400
+ "artist_name": artist->name,
401
+ artist,
402
+ difficulty,
403
+ difficulty_string,
404
+ web_url_path,
405
+ published_on
406
+ }`
407
+ return fetchSanity(query);
408
+ }
409
+
410
+ /**
411
+ * Fetch content by an array of Railcontent IDs.
412
+ * @param {Array<string>} ids - The array of Railcontent IDs of the content to fetch.
413
+ * @returns {Promise<Array<Object>|null>} - The fetched content data or null if not found.
414
+ */
415
+ async function fetchByRailContentIds(ids) {
416
+ const idsString = ids.join(',');
417
+ const query = `*[railcontent_id in [${idsString}]]{
418
+ railcontent_id,
419
+ title,
420
+ "image": thumbnail.asset->url,
421
+ "artist_name": artist->name,
422
+ artist,
423
+ difficulty,
424
+ difficulty_string,
425
+ web_url_path,
426
+ published_on
427
+ }`
428
+ return fetchSanity(query);
429
+ }
430
+
431
+ /**
432
+ * Fetch all content for a specific brand and type with pagination, search, and grouping options.
433
+ * @param {string} brand - The brand for which to fetch content.
434
+ * @param {string} type - The content type to fetch (e.g., 'song', 'artist').
435
+ * @param {Object} params - Parameters for pagination, filtering, sorting, and grouping.
436
+ * @param {number} [params.page=1] - The page number for pagination.
437
+ * @param {number} [params.limit=10] - The number of items per page.
438
+ * @param {string} [params.searchTerm=""] - The search term to filter content by title or artist.
439
+ * @param {string} [params.sort="-published_on"] - The field to sort the content by.
440
+ * @param {Array<string>} [params.includedFields=[]] - The fields to include in the query.
441
+ * @param {string} [params.groupBy=""] - The field to group the results by (e.g., 'artist', 'genre').
442
+ * @returns {Promise<Object|null>} - The fetched content data or null if not found.
443
+ */
444
+ async function fetchAll(brand, type, { page = 1, limit = 10, searchTerm = "", sort = "-published_on", includedFields = [], groupBy = "" }) {
445
+ const start = (page - 1) * limit;
446
+ const end = start + limit;
447
+
448
+ // Construct the search filter
449
+ const searchFilter = searchTerm
450
+ ? `&& (artist->name match "${searchTerm}*" || title match "${searchTerm}*")`
451
+ : "";
452
+
453
+ // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
454
+ const includedFieldsFilter = includedFields.length > 0
455
+ ? includedFields.map(field => {
456
+ let [key, value] = field.split(',');
457
+ if (key === 'difficulty') {
458
+ key = 'difficulty_string';
459
+ }
460
+ return `&& ${key} == "${value}"`;
461
+ }).join(' ')
462
+ : "";
463
+
464
+ // Determine the sort order
465
+ let sortOrder;
466
+ switch (sort) {
467
+ case "slug":
468
+ sortOrder = "artist->name asc";
469
+ break;
470
+ case "published_on":
471
+ sortOrder = "published_on desc";
472
+ break;
473
+ case "-published_on":
474
+ sortOrder = "published_on asc";
475
+ break;
476
+ case "-slug":
477
+ sortOrder = "artist->name desc";
478
+ break;
479
+ case "-popularity":
480
+ sortOrder = "popularity desc";
481
+ break;
482
+ default:
483
+ sortOrder = "published_on asc";
484
+ break;
485
+ }
486
+
487
+ // Determine the group by clause
488
+ let query = "";
489
+ if (groupBy !== "") {
490
+ query = `
491
+ {
492
+ "total": count(*[_type == '${groupBy}' && count(*[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]._id) > 0]),
493
+ "entity": *[_type == '${groupBy}' && count(*[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]._id) > 0]
494
+ {
495
+ 'id': _id,
496
+ 'type': _type,
497
+ name,
498
+ 'head_shot_picture_url': thumbnail_url.asset->url,
499
+ 'all_lessons_count': count(*[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]._id),
500
+ 'lessons': *[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]{
501
+ railcontent_id,
502
+ title,
503
+ "image": thumbnail.asset->url,
504
+ difficulty,
505
+ difficulty_string,
506
+ web_url_path,
507
+ published_on,
508
+ ${groupBy}
509
+ }[0...10]
510
+ }
511
+ |order(${sortOrder})
512
+ [${start}...${end}]
513
+ }`;
514
+ } else {
515
+ query = `
516
+ {
517
+ "entity": *[_type == '${type}' && brand == "${brand}" ${searchFilter} ${includedFieldsFilter}] | order(${sortOrder}) [${start}...${end}] {
518
+ railcontent_id,
519
+ title,
520
+ "image": thumbnail.asset->url,
521
+ difficulty,
522
+ difficulty_string,
523
+ web_url_path,
524
+ published_on
525
+ },
526
+ "total": count(*[_type == '${type}' && brand == "${brand}" ${searchFilter} ${includedFieldsFilter}])
527
+ }
528
+ `;
529
+ }
530
+
531
+ return fetchSanity(query);
532
+ }
533
+
534
+ /**
535
+ * Fetch children content by Railcontent ID.
536
+ * @param {string} railcontentId - The Railcontent ID of the parent content.
537
+ * @returns {Promise<Array<Object>|null>} - The fetched children content data or null if not found.
538
+ */
539
+ async function fetchChildren(railcontentId) {
540
+ //TODO: Implement getByParentId include sum XP
541
+ const query = `*[_railcontent_id == ${railcontentId}]{
542
+ railcontent_id,
543
+ title,
544
+ "image": thumbnail.asset->url,
545
+ "artist_name": artist->name,
546
+ artist,
547
+ difficulty,
548
+ difficulty_string,
549
+ web_url_path,
550
+ published_on
551
+ } | order(published_on asc)`
552
+ return fetchSanity(query);
553
+ }
554
+
555
+ /**
556
+ * Fetch the next lesson for a specific method by Railcontent ID.
557
+ * @param {string} railcontentId - The Railcontent ID of the current lesson.
558
+ * @returns {Promise<Object|null>} - The fetched next lesson data or null if not found.
559
+ */
560
+ async function fetchMethodNextLesson(railcontentId) {
561
+ //TODO: Implement getNextContentForParentContentForUser
562
+ const query = `*[_railcontent_id == ${railcontentId}]{
563
+ railcontent_id,
564
+ title,
565
+ "image": thumbnail.asset->url,
566
+ "artist_name": artist->name,
567
+ artist,
568
+ difficulty,
569
+ difficulty_string,
570
+ web_url_path,
571
+ published_on
572
+ }`
573
+ return fetchSanity(query);
574
+ }
575
+
576
+ /**
577
+ * Fetch all children of a specific method by Railcontent ID.
578
+ * @param {string} railcontentId - The Railcontent ID of the method.
579
+ * @returns {Promise<Array<Object>|null>} - The fetched children data or null if not found.
580
+ */
581
+ async function fetchMethodChildren(railcontentId) {
582
+ //TODO: Implement getByParentId include sum XP
583
+ return fetchChildren(railcontentId);
584
+ }
585
+
586
+ /**
587
+ * Fetch the next and previous lessons for a specific lesson by Railcontent ID.
588
+ * @param {string} railcontentId - The Railcontent ID of the current lesson.
589
+ * @returns {Promise<Object|null>} - The fetched next and previous lesson data or null if found.
590
+ */
591
+ async function fetchNextPreviousLesson(railcontentId) {
592
+ //TODO: Implement getTypeNeighbouringSiblings/getNextAndPreviousLessons
593
+ const query = `*[_railcontent_id == ${railcontentId}]{
594
+ railcontent_id,
595
+ title,
596
+ "image": thumbnail.asset->url,
597
+ "artist_name": artist->name,
598
+ artist,
599
+ difficulty,
600
+ difficulty_string,
601
+ web_url_path,
602
+ published_on
603
+ }`
604
+ return fetchSanity(query);
605
+ }
606
+
607
+ /**
608
+ * Fetch related lessons for a specific lesson by Railcontent ID and type.
609
+ * @param {string} railcontentId - The Railcontent ID of the current lesson.
610
+ * @param {string} type - The type of related lessons to fetch.
611
+ * @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
612
+ */
613
+ async function fetchRelatedLessons(railcontentId, type) {
614
+ let sort = 'published_on'
615
+ if (type == 'rhythmic-adventures-of-captain-carson' ||
616
+ type == 'diy-drum-experiments' ||
617
+ type == 'in-rhythm') {
618
+ sort = 'sort';
619
+ }
620
+ //TODO: Implement $this->contentService->getFiltered
621
+ const query = `*[_railcontent_id == ${railcontentId}]{
622
+ railcontent_id,
623
+ title,
624
+ "image": thumbnail.asset->url,
625
+ "artist_name": artist->name,
626
+ artist,
627
+ difficulty,
628
+ difficulty_string,
629
+ web_url_path,
630
+ published_on
631
+ } | order(published_on asc)[0...5]`
632
+ return fetchSanity(query);
633
+ }
634
+
635
+ /**
636
+ * Fetch all content for a specific pack by Railcontent ID.
637
+ * @param {string} railcontentId - The Railcontent ID of the pack.
638
+ * @returns {Promise<Array<Object>|null>} - The fetched pack content data or null if not found.
639
+ */
640
+ async function fetchPackAll(railcontentId) {
641
+ //TODO: Implement getPacks
642
+ const query = `*[_railcontent_id == ${railcontentId}]{
643
+ railcontent_id,
644
+ title,
645
+ "image": thumbnail.asset->url,
646
+ "artist_name": artist->name,
647
+ artist,
648
+ difficulty,
649
+ difficulty_string,
650
+ web_url_path,
651
+ published_on
652
+ } | order(published_on asc)[0...5]`
653
+ return fetchSanity(query);
654
+ }
655
+
656
+ /**
657
+ * Fetch all children of a specific pack by Railcontent ID.
658
+ * @param {string} railcontentId - The Railcontent ID of the pack.
659
+ * @returns {Promise<Array<Object>|null>} - The fetched pack children data or null if not found.
660
+ */
661
+ async function fetchPackChildren(railcontentId) {
662
+ return fetchChildren(railcontentId, 'pack');
663
+ }
664
+
665
+ /**
666
+ * Fetch data from the Sanity API based on a provided query.
667
+ * @param {string} query - The GROQ query to execute against the Sanity API.
668
+ * @returns {Promise<Object|null>} - The first result from the query, or null if an error occurs or no results are found.
669
+ */
670
+ async function fetchSanity(query, isList = false) {
671
+ // Check the config object before proceeding
672
+ if (!checkConfig(globalConfig)) {
673
+ return null;
674
+ }
675
+
676
+ if (globalConfig.debug) {
677
+ console.log("fetchSanity Query:", query);
678
+ }
679
+
680
+ const encodedQuery = encodeURIComponent(query);
681
+ const url = `https://${globalConfig.projectId}.apicdn.sanity.io/v${globalConfig.version}/data/query/${globalConfig.dataset}?query=${encodedQuery}`;
682
+ const headers = {
683
+ 'Authorization': `Bearer ${globalConfig.token}`,
684
+ 'Content-Type': 'application/json'
685
+ };
686
+
687
+ try {
688
+ const response = await fetch(url, { headers });
689
+ const result = await response.json();
690
+ if (result.result) {
691
+ if (globalConfig.debug) {
692
+ console.log("fetchSanity Results:", result.result);
693
+ }
694
+ return isList ? result.result : result.result[0];
695
+ } else {
696
+ throw new Error('No results found');
697
+ }
698
+ } catch (error) {
699
+ console.error('fetchSanity: Fetch error:', error);
700
+ return null;
701
+ }
702
+ }
703
+
704
+
705
+ //Helper Functions
706
+ function arrayJoinWithQuotes(array, delimiter = ',') {
707
+ const wrapped = array.map(value => `'${value}'`);
708
+ return wrapped.join(delimiter)
709
+ }
710
+
711
+ function getSanityDate(date) {
712
+ return date.toISOString();
713
+ }
714
+
715
+ function checkConfig(config) {
716
+ if (!config.token) {
717
+ console.warn('fetchSanity: The "token" property is missing in the config object.');
718
+ return false;
719
+ }
720
+ if (!config.projectId) {
721
+ console.warn('fetchSanity: The "projectId" property is missing in the config object.');
722
+ return false;
723
+ }
724
+ if (!config.dataset) {
725
+ console.warn('fetchSanity: The "dataset" property is missing in the config object.');
726
+ return false;
727
+ }
728
+ if (!config.version) {
729
+ console.warn('fetchSanity: The "version" property is missing in the config object.');
730
+ return false;
731
+ }
732
+ return true;
733
+ }
734
+
735
+
736
+ //Main
737
+ export {
738
+ initializeSanityService,
739
+ fetchSongById,
740
+ fetchArtists,
741
+ fetchRelatedSongs,
742
+ fetchAllSongs,
743
+ fetchFilterOptions,
744
+ fetchSongCount,
745
+ fetchWorkouts,
746
+ fetchNewReleases,
747
+ fetchUpcomingEvents,
748
+ fetchByRailContentId,
749
+ fetchByRailContentIds,
750
+ fetchAll,
751
+ fetchMethodNextLesson,
752
+ fetchMethodChildren,
753
+ fetchNextPreviousLesson,
754
+ fetchRelatedLessons,
755
+ fetchPackAll,
756
+ fetchPackChildren,
757
+ };
758
+