musora-content-services 1.0.20 → 1.0.22

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 (71) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/docs/config.js.html +124 -0
  3. package/docs/index.html +2 -2
  4. package/docs/module-Config.html +669 -0
  5. package/docs/module-Railcontent-Services.html +751 -0
  6. package/docs/{global.html → module-Sanity-Services.html} +52 -501
  7. package/docs/railcontent.js.html +178 -0
  8. package/docs/sanity.js.html +994 -0
  9. package/jsdoc.json +1 -1
  10. package/package.json +1 -1
  11. package/publish.sh +0 -0
  12. package/src/index.d.ts +2 -2
  13. package/src/index.js +31 -908
  14. package/src/services/config.js +52 -0
  15. package/src/services/railcontent.js +106 -0
  16. package/src/services/sanity.js +976 -0
  17. package/test/sanityQueryService.test.js +2 -2
  18. package/docs/data/search.json +0 -1
  19. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  20. package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
  21. package/docs/fonts/OpenSans-Bold-webfont.svg +0 -1830
  22. package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
  23. package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  24. package/docs/fonts/OpenSans-BoldItalic-webfont.svg +0 -1830
  25. package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  26. package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
  27. package/docs/fonts/OpenSans-Italic-webfont.svg +0 -1830
  28. package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
  29. package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
  30. package/docs/fonts/OpenSans-Light-webfont.svg +0 -1831
  31. package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
  32. package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  33. package/docs/fonts/OpenSans-LightItalic-webfont.svg +0 -1835
  34. package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  35. package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
  36. package/docs/fonts/OpenSans-Regular-webfont.svg +0 -1831
  37. package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
  38. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  39. package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
  40. package/docs/fonts/OpenSans-Semibold-webfont.svg +0 -1830
  41. package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
  42. package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
  43. package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
  44. package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +0 -1830
  45. package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
  46. package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
  47. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  48. package/docs/index.js.html +0 -1122
  49. package/docs/scripts/core.js +0 -726
  50. package/docs/scripts/core.min.js +0 -23
  51. package/docs/scripts/resize.js +0 -90
  52. package/docs/scripts/search.min.js +0 -6
  53. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -202
  54. package/docs/scripts/third-party/fuse.js +0 -9
  55. package/docs/scripts/third-party/hljs-line-num-original.js +0 -369
  56. package/docs/scripts/third-party/hljs-line-num.js +0 -1
  57. package/docs/scripts/third-party/hljs-original.js +0 -5171
  58. package/docs/scripts/third-party/hljs.js +0 -1
  59. package/docs/scripts/third-party/popper.js +0 -5
  60. package/docs/scripts/third-party/tippy.js +0 -1
  61. package/docs/scripts/third-party/tocbot.js +0 -672
  62. package/docs/scripts/third-party/tocbot.min.js +0 -1
  63. package/docs/styles/clean-jsdoc-theme-base.css +0 -1159
  64. package/docs/styles/clean-jsdoc-theme-dark.css +0 -412
  65. package/docs/styles/clean-jsdoc-theme-light.css +0 -482
  66. package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -30
  67. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -1
  68. package/docs/styles/clean-jsdoc-theme.min.css +0 -1
  69. package/docs/styles/jsdoc-default.css +0 -692
  70. package/docs/styles/prettify-jsdoc.css +0 -111
  71. package/docs/styles/prettify-tomorrow.css +0 -132
@@ -0,0 +1,976 @@
1
+ /**
2
+ * @module Sanity-Services
3
+ */
4
+
5
+ const { contentTypeConfig } = require('../contentTypeConfig.js');
6
+ const { globalConfig } = require('./config');
7
+
8
+ import { fetchAllCompletedStates, fetchCurrentSongComplete } from './railcontent.js';
9
+
10
+ /**
11
+ * Fetch a song by its document ID from Sanity.
12
+ *
13
+ * @param {string} documentId - The ID of the document to fetch.
14
+ * @returns {Promise<Object|null>} - A promise that resolves to an object containing the song data or null if not found.
15
+ *
16
+ * @example
17
+ * fetchSongById('abc123')
18
+ * .then(song => console.log(song))
19
+ * .catch(error => console.error(error));
20
+ */
21
+ export async function fetchSongById(songId) {
22
+ const query = `
23
+ *[_type == "song" && railcontent_id == ${songId}]{
24
+ title,
25
+ "thumbnail_url": thumbnail.asset->url,
26
+ "style": genre[0]->name,
27
+ "artist": artist->name,
28
+ album,
29
+ instrumentless,
30
+ soundslice,
31
+ railcontent_id,
32
+ "resources": resource[]{resource_url, resource_name},
33
+ }`;
34
+
35
+ const songData = await fetchSanity(query, false);
36
+
37
+ // Check if the necessary user information is available in globalConfig
38
+ if (!songData || !globalConfig.railcontentConfig.userId || !globalConfig.railcontentConfig.token) {
39
+ return songData;
40
+ }
41
+
42
+ // Fetch the completion state for the current song
43
+ const currentSongComplete = await fetchCurrentSongComplete(songId);
44
+
45
+ if (currentSongComplete) {
46
+ songData.completed = currentSongComplete.state !== "not started";
47
+ songData.progress_percent = currentSongComplete.percent.toString();
48
+ }
49
+
50
+ return songData;
51
+ }
52
+
53
+ /**
54
+ * Fetch all artists with lessons available for a specific brand.
55
+ *
56
+ * @param {string} brand - The brand for which to fetch artists.
57
+ * @returns {Promise<Object|null>} - A promise that resolves to an array of artist objects or null if not found.
58
+ *
59
+ * @example
60
+ * fetchArtists('drumeo')
61
+ * .then(artists => console.log(artists))
62
+ * .catch(error => console.error(error));
63
+ */
64
+ export async function fetchArtists(brand) {
65
+ const query = `
66
+ *[_type == "artist"]{
67
+ name,
68
+ "lessonsCount": count(*[_type == "song" && brand == "${brand}" && references(^._id)])
69
+ }[lessonsCount > 0]`;
70
+ return fetchSanity(query, true);
71
+ }
72
+
73
+ /**
74
+ * Fetch current number of artists for songs within a brand.
75
+ * @param {string} brand - The current brand.
76
+ * @returns {Promise<int|null>} - The fetched count of artists.
77
+ */
78
+ export async function fetchSongArtistCount(brand) {
79
+ const query = `count(*[_type == 'artist']{'lessonsCount': count(*[_type == 'song' && brand == '${brand}' && references(^._id)]._id)}[lessonsCount > 0])`;
80
+ return fetchSanity(query, true);
81
+ }
82
+
83
+ /**
84
+ * Fetch related songs for a specific brand and song ID.
85
+ *
86
+ * @param {string} brand - The brand for which to fetch related songs.
87
+ * @param {string} songId - The ID of the song to find related songs for.
88
+ * @returns {Promise<Object|null>} - A promise that resolves to an array of related song objects or null if not found.
89
+ *
90
+ * @example
91
+ * fetchRelatedSongs('drumeo', '12345')
92
+ * .then(relatedSongs => console.log(relatedSongs))
93
+ * .catch(error => console.error(error));
94
+ */
95
+ export async function fetchRelatedSongs(brand, songId) {
96
+ const query = `
97
+ *[_type == "song" && railcontent_id == ${songId}]{
98
+ "data": array::unique([
99
+ ...(*[_type == "song" && brand == "${brand}" && railcontent_id != ${songId} && references(^.artist->_id)]{
100
+ "type": _type,
101
+ "id": railcontent_id,
102
+ "url": web_url_path,
103
+ "published_on": published_on,
104
+ status,
105
+ "fields": [
106
+ {
107
+ "key": "title",
108
+ "value": title
109
+ },
110
+ {
111
+ "key": "artist",
112
+ "value": artist->name
113
+ },
114
+ {
115
+ "key": "difficulty",
116
+ "value": difficulty
117
+ },
118
+ {
119
+ "key": "length_in_seconds",
120
+ "value": soundslice[0].soundslice_length_in_second
121
+ }
122
+ ],
123
+ "data": [{
124
+ "key": "thumbnail_url",
125
+ "value": thumbnail.asset->url
126
+ }]
127
+ }[0...10]),
128
+ ...(*[_type == "song" && brand == "${brand}" && railcontent_id != ${songId} && references(^.genre[]->_id)]{
129
+ "type": _type,
130
+ "id": railcontent_id,
131
+ "url": web_url_path,
132
+ "published_on": published_on,
133
+ status,
134
+ "fields": [
135
+ {
136
+ "key": "title",
137
+ "value": title
138
+ },
139
+ {
140
+ "key": "artist",
141
+ "value": artist->name
142
+ },
143
+ {
144
+ "key": "difficulty",
145
+ "value": difficulty
146
+ },
147
+ {
148
+ "key": "length_in_seconds",
149
+ "value": soundslice[0].soundslice_length_in_second
150
+ }
151
+ ],
152
+ "data": [{
153
+ "key": "thumbnail_url",
154
+ "value": thumbnail.asset->url
155
+ }]
156
+ }[0...10])
157
+ ])[0...10]
158
+ }`;
159
+
160
+ // Fetch the related songs data
161
+ const relatedSongsData = await fetchSanity(query, true);
162
+
163
+ if (!relatedSongsData || !userId || !token) {
164
+ return relatedSongsData;
165
+ }
166
+
167
+ // Extract the IDs of related lessons
168
+ const relatedLessonIds = relatedSongsData.data.map(lesson => lesson.id);
169
+
170
+ // Fetch the completion states for the related lessons
171
+ const relatedLessonsCompletionStates = await fetchAllCompletedStates(relatedLessonIds);
172
+
173
+ // Map the completion states to the related lessons
174
+ relatedSongsData.data = relatedSongsData.data.map(lesson => {
175
+ const lessonCompletionState = relatedLessonsCompletionStates[lesson.id];
176
+ if (lessonCompletionState) {
177
+ return {
178
+ ...lesson,
179
+ completed: lessonCompletionState.state !== "not started",
180
+ lesson_progress: lessonCompletionState.percent.toString()
181
+ };
182
+ }
183
+ return lesson;
184
+ });
185
+
186
+ return relatedSongsData;
187
+ }
188
+
189
+ /**
190
+ * Fetch all songs for a specific brand with pagination and search options.
191
+ * @param {string} brand - The brand for which to fetch songs.
192
+ * @param {Object} params - Parameters for pagination, filtering, and sorting.
193
+ * @param {number} [params.page=1] - The page number for pagination.
194
+ * @param {number} [params.limit=10] - The number of songs per page.
195
+ * @param {string} [params.searchTerm=""] - The search term to filter songs by title or artist.
196
+ * @param {string} [params.sort="-published_on"] - The field to sort the songs by.
197
+ * @param {Array<string>} [params.includedFields=[]] - The fields to include in the query.
198
+ * @param {string} [params.groupBy=""] - The field to group the results by.
199
+ * @returns {Promise<Object|null>} - The fetched song data or null if not found.
200
+ *
201
+ * @example
202
+ * fetchAllSongs('drumeo', {
203
+ * page: 2,
204
+ * limit: 20,
205
+ * searchTerm: 'rock',
206
+ * sort: 'published_on',
207
+ * includedFields: ['difficulty', 'style'],
208
+ * groupBy: 'artist'
209
+ * })
210
+ * .then(result => console.log(result))
211
+ * .catch(error => console.error(error));
212
+ */
213
+ export async function fetchAllSongs(brand, {
214
+ page = 1,
215
+ limit = 10,
216
+ searchTerm = "",
217
+ sort = "-published_on",
218
+ includedFields = [],
219
+ groupBy = ""
220
+ }) {
221
+ return fetchAll(brand, 'song', {page, limit, searchTerm, sort, includedFields, groupBy});
222
+ }
223
+
224
+ /**
225
+ * Fetch filter options for a specific brand.
226
+ *
227
+ * @param {string} brand - The brand for which to fetch filter options.
228
+ * @returns {Promise<Object|null>} - A promise that resolves to an object containing filter options or null if not found.
229
+ *
230
+ * @example
231
+ * fetchSongFilterOptions('drumeo')
232
+ * .then(options => console.log(options))
233
+ * .catch(error => console.error(error));
234
+ */
235
+ export async function fetchSongFilterOptions(brand) {
236
+ const query = `
237
+ {
238
+ "difficulty": [
239
+ {"type": "Introductory", "count": count(*[_type == 'song' && brand == '${brand}' && difficulty_string == "Introductory"]._id)},
240
+ {"type": "Beginner", "count": count(*[_type == 'song' && brand == '${brand}' && difficulty_string == "Beginner"]._id)},
241
+ {"type": "Intermediate", "count": count(*[_type == 'song' && brand == '${brand}' && difficulty_string == "Intermediate"]._id)},
242
+ {"type": "Advanced", "count": count(*[_type == 'song' && brand == '${brand}' && difficulty_string == "Advanced"]._id)},
243
+ {"type": "Expert", "count": count(*[_type == 'song' && brand == '${brand}' && difficulty_string == "Expert"]._id)}
244
+ ],
245
+ "genre": *[_type == 'genre' && 'song' in filter_types] {
246
+ "type": name,
247
+ "count": count(*[_type == 'song' && brand == '${brand}' && references(^._id)]._id)
248
+ },
249
+ "instrumentless": [
250
+ {"type": "Full Song Only", "count": count(*[_type == 'song' && brand == '${brand}' && instrumentless == false]._id)},
251
+ {"type": "Instrument Removed", "count": count(*[_type == 'song' && brand == '${brand}' && instrumentless == true]._id)}
252
+ ]
253
+ }
254
+ `;
255
+
256
+ return fetchSanity(query, true);
257
+ }
258
+
259
+ /**
260
+ * Fetch the total count of songs for a specific brand.
261
+ * @param {string} brand - The brand for which to fetch the song count.
262
+ * @returns {Promise<number|null>} - The total count of songs or null if an error occurs.
263
+ */
264
+ export async function fetchSongCount(brand) {
265
+ const query = `count(*[_type == 'song' && brand == "${brand}"])`;
266
+ return fetchSanity(query, false);
267
+ }
268
+
269
+ /**
270
+ * Fetch the latest workouts for the home page of a specific brand.
271
+ * This function retrieves the latest workout content for a given brand, fetching up to five workouts. The workouts are sorted in descending order by their publication date.
272
+ * @param {string} brand - The brand for which to fetch workouts (e.g., 'drumeo', 'pianote').
273
+ * @returns {Promise<Array<Object>|null>} - A promise that resolves to an array of workout data objects or null if no workouts are found.
274
+ *
275
+ * @example
276
+ * fetchWorkouts('drumeo')
277
+ * .then(workouts => console.log(workouts))
278
+ * .catch(error => console.error(error));
279
+ */
280
+ export async function fetchWorkouts(brand) {
281
+ const query = `*[_type == 'workout' && brand == '${brand}'] [0...5] {
282
+ "id": railcontent_id,
283
+ title,
284
+ "image": thumbnail.asset->url,
285
+ "artist_name": instructor[0]->name,
286
+ "artists": instructor[]->name,
287
+ difficulty,
288
+ difficulty_string,
289
+ length_in_seconds,
290
+ published_on,
291
+ "type": _type,
292
+ web_url_path,
293
+ } | order(published_on desc)[0...5]`
294
+ return fetchSanity(query, true);
295
+ }
296
+
297
+ /**
298
+ * Fetch the latest new releases for a specific brand.
299
+ * @param {string} brand - The brand for which to fetch new releases.
300
+ * @returns {Promise<Object|null>} - The fetched new releases data or null if not found.
301
+ */
302
+ export async function fetchNewReleases(brand) {
303
+ const newTypes = {
304
+ '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"],
305
+ '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"],
306
+ '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"],
307
+ '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"],
308
+ '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"]
309
+ };
310
+ const typesString = arrayJoinWithQuotes(newTypes[brand] ?? newTypes['default']);
311
+ const query = `*[_type in [${typesString}] && brand == '${brand}'] | order(releaseDate desc) [0...5] {
312
+ "id": railcontent_id,
313
+ title,
314
+ "image": thumbnail.asset->url,
315
+ "artist_name": instructor[0]->name,
316
+ "artists": instructor[]->name,
317
+ difficulty,
318
+ difficulty_string,
319
+ length_in_seconds,
320
+ published_on,
321
+ "type": _type,
322
+ web_url_path,
323
+ } | order(published_on desc)[0...5]`
324
+ return fetchSanity(query, true);
325
+ }
326
+
327
+
328
+ /**
329
+ * Fetch upcoming events for a specific brand that are within 48 hours before their `published_on` date
330
+ * and are currently ongoing based on their `length_in_seconds`.
331
+ *
332
+ * This function retrieves events that have a `published_on` date within the last 48 hours or are currently
333
+ * ongoing based on the event's duration (`length_in_seconds`).
334
+ *
335
+ * @param {string} brand - The brand for which to fetch upcoming events (e.g., 'drumeo', 'pianote', etc.).
336
+ * @returns {Promise<Array<Object>|null>} - A promise that resolves to an array of event objects or null if no events are found.
337
+ *
338
+ * @example
339
+ * // Example usage:
340
+ * fetchLiveEvent('drumeo')
341
+ * .then(events => console.log(events))
342
+ * .catch(error => console.error(error));
343
+ *
344
+ */
345
+ export async function fetchLiveEvent(brand) {
346
+ const baseLiveTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"];
347
+ const liveTypes = {
348
+ 'drumeo': [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "student-collaborations", "live-streams", "podcasts", "solos", "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"],
349
+ 'pianote': baseLiveTypes,
350
+ 'guitareo': [...baseLiveTypes, "archives"],
351
+ 'singeo': baseLiveTypes,
352
+ 'default': baseLiveTypes
353
+ };
354
+
355
+ const typesString = arrayJoinWithQuotes(liveTypes[brand] ?? liveTypes['default']);
356
+ const now = getSanityDate(new Date());
357
+ const twoDaysAgo = getSanityDate(new Date(Date.now() - 48 * 60 * 60 * 1000)); // 48 hours ago
358
+
359
+ // Adjust the query to filter events based on the calculated time window
360
+ const query = `
361
+ *[_type in [${typesString}] && brand == '${brand}'
362
+ && published_on > '${twoDaysAgo}'
363
+ || (published_on <= '${now}' && dateTime(published_on) + length_in_seconds * 1000 > '${now}')
364
+ ] {
365
+ "id": railcontent_id,
366
+ title,
367
+ "image": thumbnail.asset->url,
368
+ "artist_name": instructor[0]->name,
369
+ "artists": instructor[]->name,
370
+ difficulty,
371
+ difficulty_string,
372
+ length_in_seconds,
373
+ published_on,
374
+ "type": _type,
375
+ web_url_path,
376
+ } | order(published_on asc)[0]`;
377
+
378
+ return fetchSanity(query, true);
379
+ }
380
+
381
+ /**
382
+ * Fetch upcoming events for a specific brand.
383
+ *
384
+ * @param {string} brand - The brand for which to fetch upcoming events.
385
+ * @returns {Promise<Object|null>} - A promise that resolves to an array of upcoming event objects or null if not found.
386
+ *
387
+ * @example
388
+ * fetchUpcomingEvents('drumeo')
389
+ * .then(events => console.log(events))
390
+ * .catch(error => console.error(error));
391
+ */
392
+ export async function fetchUpcomingEvents(brand) {
393
+ const baseLiveTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"];
394
+ const liveTypes = {
395
+ 'drumeo': [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "student-collaborations", "live-streams", "podcasts", "solos", "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"],
396
+ 'pianote': baseLiveTypes,
397
+ 'guitareo': [...baseLiveTypes, "archives"],
398
+ 'singeo': baseLiveTypes,
399
+ 'default': baseLiveTypes
400
+ };
401
+ const typesString = arrayJoinWithQuotes(liveTypes[brand] ?? liveTypes['default']);
402
+ const now = getSanityDate(new Date());
403
+ //TODO: status = 'scheduled' is this handled in sanity?
404
+ const query = `*[_type in [${typesString}] && brand == '${brand}' && published_on > '${now}']{
405
+ "id": railcontent_id,
406
+ title,
407
+ "image": thumbnail.asset->url,
408
+ "artist_name": instructor[0]->name,
409
+ "artists": instructor[]->name,
410
+ difficulty,
411
+ difficulty_string,
412
+ length_in_seconds,
413
+ published_on,
414
+ "type": _type,
415
+ web_url_path,
416
+ } | order(published_on asc)[0...5]`;
417
+ return fetchSanity(query, true);
418
+ }
419
+
420
+ /**
421
+ * Fetch upcoming events for a specific brand that are within 48 hours before their `published_on` date
422
+ * and are currently ongoing based on their `length_in_seconds`.
423
+ *
424
+ * This function retrieves events that have a `published_on` date within the last 48 hours or are currently
425
+ * ongoing based on the event's duration (`length_in_seconds`).
426
+ *
427
+ * @param {string} brand - The brand for which to fetch upcoming events (e.g., 'drumeo', 'pianote', etc.).
428
+ * @returns {Promise<Array<Object>|null>} - A promise that resolves to an array of event objects or null if no events are found.
429
+ *
430
+ * @example
431
+ * // Example usage:
432
+ * fetchUpcomingEvents('drumeo')
433
+ * .then(events => console.log(events))
434
+ * .catch(error => console.error(error));
435
+ *
436
+ */
437
+ export async function fetchLiveEvent(brand) {
438
+ const baseLiveTypes = ["student-review", "student-reviews", "student-focus", "coach-stream", "live", "question-and-answer", "student-review", "boot-camps", "recording", "pack-bundle-lesson"];
439
+ const liveTypes = {
440
+ 'drumeo': [...baseLiveTypes, "drum-fest-international-2022", "spotlight", "the-history-of-electronic-drums", "backstage-secrets", "quick-tips", "student-collaborations", "live-streams", "podcasts", "solos", "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"],
441
+ 'pianote': baseLiveTypes,
442
+ 'guitareo': [...baseLiveTypes, "archives"],
443
+ 'singeo': baseLiveTypes,
444
+ 'default': baseLiveTypes
445
+ };
446
+
447
+ const typesString = arrayJoinWithQuotes(liveTypes[brand] ?? liveTypes['default']);
448
+ const now = getSanityDate(new Date());
449
+ const twoDaysAgo = getSanityDate(new Date(Date.now() - 48 * 60 * 60 * 1000)); // 48 hours ago
450
+
451
+ // Adjust the query to filter events based on the calculated time window
452
+ const query = `
453
+ *[_type in [${typesString}] && brand == '${brand}'
454
+ && published_on > '${twoDaysAgo}'
455
+ || (published_on <= '${now}' && dateTime(published_on) + length_in_seconds * 1000 > '${now}')
456
+ ] {
457
+ "id": railcontent_id,
458
+ title,
459
+ "image": thumbnail.asset->url,
460
+ "artist_name": instructor[0]->name,
461
+ "artists": instructor[]->name,
462
+ difficulty,
463
+ difficulty_string,
464
+ length_in_seconds,
465
+ published_on,
466
+ "type": _type,
467
+ web_url_path,
468
+ } | order(published_on asc)[0]`;
469
+
470
+ return fetchSanity(query, true);
471
+ }
472
+
473
+
474
+ /**
475
+ * Fetch content by a specific Railcontent ID.
476
+ *
477
+ * @param {string} id - The Railcontent ID of the content to fetch.
478
+ * @returns {Promise<Object|null>} - A promise that resolves to the content object or null if not found.
479
+ *
480
+ * @example
481
+ * fetchByRailContentId('abc123')
482
+ * .then(content => console.log(content))
483
+ * .catch(error => console.error(error));
484
+ */
485
+ export async function fetchByRailContentId(id) {
486
+ const query = `*[railcontent_id == ${id}]{
487
+ railcontent_id,
488
+ title,
489
+ "image": thumbnail.asset->url,
490
+ "artist_name": artist->name,
491
+ artist,
492
+ difficulty,
493
+ difficulty_string,
494
+ web_url_path,
495
+ published_on
496
+ }`
497
+ return fetchSanity(query, false);
498
+ }
499
+
500
+ /**
501
+ * Fetch content by an array of Railcontent IDs.
502
+ *
503
+ * @param {Array<string>} ids - The array of Railcontent IDs of the content to fetch.
504
+ * @returns {Promise<Array<Object>|null>} - A promise that resolves to an array of content objects or null if not found.
505
+ *
506
+ * @example
507
+ * fetchByRailContentIds(['abc123', 'def456', 'ghi789'])
508
+ * .then(contents => console.log(contents))
509
+ * .catch(error => console.error(error));
510
+ */
511
+ export async function fetchByRailContentIds(ids) {
512
+ const idsString = ids.join(',');
513
+ const query = `*[railcontent_id in [${idsString}]]{
514
+ railcontent_id,
515
+ title,
516
+ "image": thumbnail.asset->url,
517
+ "artist_name": artist->name,
518
+ artist,
519
+ difficulty,
520
+ difficulty_string,
521
+ web_url_path,
522
+ published_on
523
+ }`
524
+ return fetchSanity(query, true);
525
+ }
526
+
527
+ /**
528
+ * Fetch all content for a specific brand and type with pagination, search, and grouping options.
529
+ * @param {string} brand - The brand for which to fetch content.
530
+ * @param {string} type - The content type to fetch (e.g., 'song', 'artist').
531
+ * @param {Object} params - Parameters for pagination, filtering, sorting, and grouping.
532
+ * @param {number} [params.page=1] - The page number for pagination.
533
+ * @param {number} [params.limit=10] - The number of items per page.
534
+ * @param {string} [params.searchTerm=""] - The search term to filter content by title or artist.
535
+ * @param {string} [params.sort="-published_on"] - The field to sort the content by.
536
+ * @param {Array<string>} [params.includedFields=[]] - The fields to include in the query.
537
+ * @param {string} [params.groupBy=""] - The field to group the results by (e.g., 'artist', 'genre').
538
+ * @returns {Promise<Object|null>} - The fetched content data or null if not found.
539
+ *
540
+ * @example
541
+ * fetchAll('drumeo', 'song', {
542
+ * page: 2,
543
+ * limit: 20,
544
+ * searchTerm: 'jazz',
545
+ * sort: '-popularity',
546
+ * includedFields: ['difficulty,Intermediate'],
547
+ * groupBy: 'artist'
548
+ * })
549
+ * .then(content => console.log(content))
550
+ * .catch(error => console.error(error));
551
+ */
552
+ export async function fetchAll(brand, type, {
553
+ page = 1,
554
+ limit = 10,
555
+ searchTerm = "",
556
+ sort = "-published_on",
557
+ includedFields = [],
558
+ groupBy = ""
559
+ }) {
560
+ let config = contentTypeConfig[type] ?? {};
561
+ let additionalFields = config?.fields ?? [];
562
+ let isGroupByOneToOne = (groupBy ? config?.relationships[groupBy]?.isOneToOne : false) ?? false;
563
+ const start = (page - 1) * limit;
564
+ const end = start + limit;
565
+
566
+ // Construct the search filter
567
+ const searchFilter = searchTerm
568
+ ? `&& (artist->name match "${searchTerm}*" || title match "${searchTerm}*")`
569
+ : "";
570
+
571
+ // Construct the included fields filter, replacing 'difficulty' with 'difficulty_string'
572
+ const includedFieldsFilter = includedFields.length > 0
573
+ ? includedFields.map(field => {
574
+ let [key, value] = field.split(',');
575
+ if (key === 'difficulty') {
576
+ key = 'difficulty_string';
577
+ }
578
+ return `&& ${key} == "${value}"`;
579
+ }).join(' ')
580
+ : "";
581
+
582
+ // Determine the sort order
583
+ let sortOrder;
584
+ switch (sort) {
585
+ case "slug":
586
+ sortOrder = "artist->name asc";
587
+ break;
588
+ case "published_on":
589
+ sortOrder = "published_on desc";
590
+ break;
591
+ case "-published_on":
592
+ sortOrder = "published_on asc";
593
+ break;
594
+ case "-slug":
595
+ sortOrder = "artist->name desc";
596
+ break;
597
+ case "-popularity":
598
+ sortOrder = "popularity desc";
599
+ break;
600
+ default:
601
+ sortOrder = "published_on asc";
602
+ break;
603
+ }
604
+
605
+ let defaultFields = ['railcontent_id',
606
+ 'title',
607
+ '"image": thumbnail.asset->url',
608
+ 'difficulty',
609
+ 'difficulty_string',
610
+ 'web_url_path',
611
+ 'published_on'];
612
+
613
+ let fields = defaultFields.concat(additionalFields);
614
+ let fieldsString = fields.join(',');
615
+
616
+ // Determine the group by clause
617
+ let query = "";
618
+ if (groupBy !== "" && isGroupByOneToOne) {
619
+ query = `
620
+ {
621
+ "total": count(*[_type == '${groupBy}' && count(*[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]._id) > 0]),
622
+ "entity": *[_type == '${groupBy}' && count(*[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]._id) > 0]
623
+ {
624
+ 'id': _id,
625
+ 'type': _type,
626
+ name,
627
+ 'head_shot_picture_url': thumbnail_url.asset->url,
628
+ 'all_lessons_count': count(*[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]._id),
629
+ 'lessons': *[_type == '${type}' && brand == '${brand}' && ^._id == ${groupBy}._ref ]{
630
+ ${fieldsString},
631
+ ${groupBy}
632
+ }[0...10]
633
+ }
634
+ |order(${sortOrder})
635
+ [${start}...${end}]
636
+ }`;
637
+ } else if (groupBy !== "") {
638
+ query = `
639
+ {
640
+ "total": count(*[_type == '${groupBy}' && count(*[_type == '${type}' && brand == '${brand}' && ^._id in ${groupBy}[]._ref]._id)>0]),
641
+ "entity": *[_type == '${groupBy}' && count(*[_type == '${type}' && brand == '${brand}' && ^._id in ${groupBy}[]._ref]._id) > 0]
642
+ {
643
+ 'id': _id,
644
+ 'type': _type,
645
+ name,
646
+ 'head_shot_picture_url': thumbnail_url.asset->url,
647
+ 'all_lessons_count': count(*[_type == '${type}' && brand == '${brand}' && ^._id in ${groupBy}[]._ref ]._id),
648
+ 'lessons': *[_type == '${type}' && brand == '${brand}' && ^._id in ${groupBy}[]._ref ]{
649
+ ${fieldsString},
650
+ ${groupBy}
651
+ }[0...10]
652
+ }
653
+ |order(${sortOrder})
654
+ [${start}...${end}]
655
+ }`;
656
+ } else {
657
+ query = `
658
+ {
659
+ "entity": *[_type == '${type}' && brand == "${brand}" ${searchFilter} ${includedFieldsFilter}] | order(${sortOrder}) [${start}...${end}] {
660
+ ${fieldsString}
661
+ },
662
+ "total": count(*[_type == '${type}' && brand == "${brand}" ${searchFilter} ${includedFieldsFilter}])
663
+ }
664
+ `;
665
+ }
666
+
667
+ return fetchSanity(query, true);
668
+ }
669
+
670
+ /**
671
+ * Fetches all available filter options based on various criteria such as brand, filters, style, artist, content type, and search term.
672
+ *
673
+ * This function constructs a query to retrieve the total number of results and filter options such as difficulty, instrument type, and genre.
674
+ * The filter options are dynamically generated based on the provided filters, style, artist, and content type.
675
+ *
676
+ * @param {string} brand - The brand for which to fetch the filter options.
677
+ * @param {string} filters - Additional filters to apply to the query, typically in the format of Sanity GROQ queries.
678
+ * @param {string} [style] - Optional style or genre to filter the results. If provided, the query will check if the style exists in the genre array.
679
+ * @param {string} [artist] - Optional artist name to filter the results. If provided, the query will check if the artist's name matches.
680
+ * @param {string} contentType - The content type to fetch (e.g., 'song', 'lesson').
681
+ * @param {string} [term] - Optional search term to match against various fields such as title, album, artist name, and genre.
682
+ *
683
+ * @returns {Promise<Object|null>} - A promise that resolves to an object containing the total results and filter options, or null if the query fails.
684
+ *
685
+ * @example
686
+ * // Example usage:
687
+ * fetchAllFilterOptions('myBrand', '', 'Rock', 'John Doe', 'song', 'Love')
688
+ * .then(options => console.log(options))
689
+ * .catch(error => console.error(error));
690
+ */
691
+ export async function fetchAllFilterOptions(
692
+ brand,
693
+ filters,
694
+ style,
695
+ artist,
696
+ contentType,
697
+ term
698
+ ) {
699
+ const query = `
700
+ {
701
+ "meta": {
702
+ "totalResults": count(*[_type == '${contentType}' && brand == "${brand}" && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} ${filters}
703
+ ${term ? `&& (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}]),
704
+ "filterOptions": {
705
+ "difficulty": [
706
+ {"type": "Introductory", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && difficulty_string == "Introductory" ${filters}])},
707
+ {"type": "Beginner", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && difficulty_string == "Beginner" ${filters}])},
708
+ {"type": "Intermediate", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && difficulty_string == "Intermediate" ${filters}])},
709
+ {"type": "Advanced", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && difficulty_string == "Advanced" ${filters}])},
710
+ {"type": "Expert", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && difficulty_string == "Expert" ${filters}])}
711
+ ][count > 0],
712
+ "instrumentless": [
713
+ {"type": "Full Song Only", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && instrumentless == false ${filters}])},
714
+ {"type": "Instrument Removed", "count": count(*[_type == '${contentType}' && brand == '${brand}' && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && instrumentless == true ${filters}])}
715
+ ][count > 0],
716
+ "genre": *[_type == 'genre' && '${contentType}' in filter_types] {
717
+ "type": name,
718
+ "count": count(*[_type == '${contentType}' && brand == "${brand}" && ${style ? `'${style}' in genre[]->name` : `artist->name == '${artist}'`} && references(^._id)])
719
+ }[count > 0]
720
+ }
721
+ }
722
+ }
723
+ }
724
+ `;
725
+
726
+ return fetchSanity(query, false);
727
+ }
728
+
729
+ /**
730
+ * Fetch children content by Railcontent ID.
731
+ * @param {string} railcontentId - The Railcontent ID of the parent content.
732
+ * @returns {Promise<Array<Object>|null>} - The fetched children content data or null if not found.
733
+ */
734
+ export async function fetchChildren(railcontentId) {
735
+ //TODO: Implement getByParentId include sum XP
736
+ const query = `*[_railcontent_id == ${railcontentId}]{
737
+ railcontent_id,
738
+ title,
739
+ "image": thumbnail.asset->url,
740
+ "artist_name": artist->name,
741
+ artist,
742
+ difficulty,
743
+ difficulty_string,
744
+ web_url_path,
745
+ published_on
746
+ } | order(published_on asc)`
747
+ return fetchSanity(query, true);
748
+ }
749
+
750
+ /**
751
+ * Fetch the next lesson for a specific method by Railcontent ID.
752
+ * @param {string} railcontentId - The Railcontent ID of the current lesson.
753
+ * @returns {Promise<Object|null>} - The fetched next lesson data or null if not found.
754
+ */
755
+ export async function fetchMethodNextLesson(railcontentId) {
756
+ //TODO: Implement getNextContentForParentContentForUser
757
+ const query = `*[_railcontent_id == ${railcontentId}]{
758
+ railcontent_id,
759
+ title,
760
+ "image": thumbnail.asset->url,
761
+ "artist_name": artist->name,
762
+ artist,
763
+ difficulty,
764
+ difficulty_string,
765
+ web_url_path,
766
+ published_on
767
+ }`
768
+ return fetchSanity(query, false);
769
+ }
770
+
771
+ /**
772
+ * Fetch all children of a specific method by Railcontent ID.
773
+ * @param {string} railcontentId - The Railcontent ID of the method.
774
+ * @returns {Promise<Array<Object>|null>} - The fetched children data or null if not found.
775
+ */
776
+ export async function fetchMethodChildren(railcontentId) {
777
+ //TODO: Implement getByParentId include sum XP
778
+ return fetchChildren(railcontentId);
779
+ }
780
+
781
+ /**
782
+ * Fetch the next and previous lessons for a specific lesson by Railcontent ID.
783
+ * @param {string} railcontentId - The Railcontent ID of the current lesson.
784
+ * @returns {Promise<Object|null>} - The fetched next and previous lesson data or null if found.
785
+ */
786
+ export async function fetchNextPreviousLesson(railcontentId) {
787
+ //TODO: Implement getTypeNeighbouringSiblings/getNextAndPreviousLessons
788
+ const query = `*[_railcontent_id == ${railcontentId}]{
789
+ railcontent_id,
790
+ title,
791
+ "image": thumbnail.asset->url,
792
+ "artist_name": artist->name,
793
+ artist,
794
+ difficulty,
795
+ difficulty_string,
796
+ web_url_path,
797
+ published_on
798
+ }`
799
+ return fetchSanity(query, false);
800
+ }
801
+
802
+ /**
803
+ * Fetch the page data for a specific lesson by Railcontent ID.
804
+ * @param {string} railContentId - The Railcontent ID of the current lesson.
805
+ * @returns {Promise<Object|null>} - The fetched page data or null if found.
806
+ *
807
+ * @example
808
+ * fetchLessonContent('lesson123')
809
+ * .then(data => console.log(data))
810
+ * .catch(error => console.error(error));
811
+ */
812
+ export async function fetchLessonContent(railContentId) {
813
+ const query = `*[railcontent_id == ${railContentId} ]
814
+ {title, published_on,"type":_type, "resources": resource, difficulty, difficulty_string, brand, soundslice, instrumentless, railcontent_id, "id":railcontent_id, slug, artist->,"thumbnail_url":thumbnail.asset->url, "url": web_url_path, soundslice_slug,description,
815
+ "chapters": chapter[]{
816
+ chapter_description,
817
+ chapter_timecode,
818
+ "chapter_thumbnail_url": chapter_thumbnail_url.asset->url
819
+ },
820
+ "coaches": instructor[]-> {
821
+ name,
822
+ "id":_id,
823
+ "coach_profile_image":thumbnail_url.asset->url
824
+ },
825
+ "instructors":instructor[]->name,
826
+ instructor[]->,
827
+ "assignments":assignment[]{
828
+ "id": railcontent_id,
829
+ "soundslice_slug": assignment_soundslice,
830
+ "title": assignment_title,
831
+ "sheet_music_image_url": assignment_sheet_music_image,
832
+ "timecode": assignment_timecode,
833
+ "description": assignment_description
834
+ },
835
+ video}`
836
+ return fetchSanity(query, false);
837
+ }
838
+
839
+ /**
840
+ * Fetch related lessons for a specific lesson by RailContent ID and type.
841
+ * @param {string} railContentId - The RailContent ID of the current lesson.
842
+ * @param {string} brand - The current brand.
843
+ * @returns {Promise<Array<Object>|null>} - The fetched related lessons data or null if not found.
844
+ */
845
+ export async function fetchRelatedLessons(railContentId, brand) {
846
+ // let sort = 'published_on'
847
+ // if (type == 'rhythmic-adventures-of-captain-carson' ||
848
+ // type == 'diy-drum-experiments' ||
849
+ // type == 'in-rhythm') {
850
+ // sort = 'sort';
851
+ // }
852
+ //TODO: Implement $this->contentService->getFiltered
853
+ const query = `*[railcontent_id == ${railContentId} && brand == "${brand}" && references(*[_type=='permission']._id)]{
854
+ "related_lessons" : array::unique([
855
+ ...(*[_type=="song" && brand == "${brand}" && references(^.artist->_id)]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->}[0...11]),
856
+ ...(*[_type=="song" && brand == "${brand}" && references(^.genre[]->_id)]{_id, "id":railcontent_id, published_on, title, "thumbnail_url":thumbnail.asset->url, difficulty_string, railcontent_id, artist->}[0...11])
857
+ ])|order(published_on, railcontent_id)[0...11]}`;
858
+ return fetchSanity(query, false);
859
+ }
860
+
861
+ /**
862
+ * Fetch all content for a specific pack by Railcontent ID.
863
+ * @param {string} railcontentId - The Railcontent ID of the pack.
864
+ * @returns {Promise<Array<Object>|null>} - The fetched pack content data or null if not found.
865
+ */
866
+ export async function fetchPackAll(railcontentId) {
867
+ //TODO: Implement getPacks
868
+ const query = `*[_railcontent_id == ${railcontentId}]{
869
+ railcontent_id,
870
+ title,
871
+ "image": thumbnail.asset->url,
872
+ "artist_name": artist->name,
873
+ artist,
874
+ difficulty,
875
+ difficulty_string,
876
+ web_url_path,
877
+ published_on
878
+ } | order(published_on asc)[0...5]`
879
+ return fetchSanity(query, true);
880
+ }
881
+
882
+ /**
883
+ * Fetch all children of a specific pack by Railcontent ID.
884
+ * @param {string} railcontentId - The Railcontent ID of the pack.
885
+ * @returns {Promise<Array<Object>|null>} - The fetched pack children data or null if not found.
886
+ *
887
+ * @example
888
+ * fetchPackChildren('pack123')
889
+ * .then(children => console.log(children))
890
+ * .catch(error => console.error(error));
891
+ */
892
+ export async function fetchPackChildren(railcontentId) {
893
+ return fetchChildren(railcontentId, 'pack');
894
+ }
895
+
896
+ /**
897
+ * Fetch data from the Sanity API based on a provided query.
898
+ *
899
+ * @param {string} query - The GROQ query to execute against the Sanity API.
900
+ * @param {boolean} isList - Whether to return an array or a single result.
901
+ * @returns {Promise<Object|null>} - A promise that resolves to the fetched data or null if an error occurs or no results are found.
902
+ *
903
+ * @example
904
+ * const query = `*[_type == "song"]{title, artist->name}`;
905
+ * fetchSanity(query, true)
906
+ * .then(data => console.log(data))
907
+ * .catch(error => console.error(error));
908
+ */
909
+ export async function fetchSanity(query, isList) {
910
+ // Check the config object before proceeding
911
+ if (!checkSanityConfig(globalConfig)) {
912
+ return null;
913
+ }
914
+
915
+ if (globalConfig.sanityConfig.debug) {
916
+ console.log("fetchSanity Query:", query);
917
+ }
918
+
919
+ const encodedQuery = encodeURIComponent(query);
920
+ const api = globalConfig.sanityConfig.useCachedAPI ? 'apicdn' : 'api';
921
+ const url = `https://${globalConfig.sanityConfig.projectId}.${api}.sanity.io/v${globalConfig.sanityConfig.version}/data/query/${globalConfig.sanityConfig.dataset}?query=${encodedQuery}`;
922
+ const headers = {
923
+ 'Authorization': `Bearer ${globalConfig.sanityConfig.token}`,
924
+ 'Content-Type': 'application/json'
925
+ };
926
+
927
+ try {
928
+ const response = await fetch(url, {headers});
929
+ if (!response.ok) {
930
+ throw new Error(`Sanity API error: ${response.status} - ${response.statusText}`);
931
+ }
932
+ const result = await response.json();
933
+ if (result.result) {
934
+ if (globalConfig.sanityConfig.debug) {
935
+ console.log("fetchSanity Results:", result);
936
+ }
937
+ return isList ? result.result : result.result[0];
938
+ } else {
939
+ throw new Error('No results found');
940
+ }
941
+ } catch (error) {
942
+ console.error('fetchSanity: Fetch error:', error);
943
+ return null;
944
+ }
945
+ }
946
+
947
+
948
+ //Helper Functions
949
+ function arrayJoinWithQuotes(array, delimiter = ',') {
950
+ const wrapped = array.map(value => `'${value}'`);
951
+ return wrapped.join(delimiter)
952
+ }
953
+
954
+ function getSanityDate(date) {
955
+ return date.toISOString();
956
+ }
957
+
958
+ function checkSanityConfig(config) {
959
+ if (!config.sanityConfig.token) {
960
+ console.warn('fetchSanity: The "token" property is missing in the config object.');
961
+ return false;
962
+ }
963
+ if (!config.sanityConfig.projectId) {
964
+ console.warn('fetchSanity: The "projectId" property is missing in the config object.');
965
+ return false;
966
+ }
967
+ if (!config.sanityConfig.dataset) {
968
+ console.warn('fetchSanity: The "dataset" property is missing in the config object.');
969
+ return false;
970
+ }
971
+ if (!config.sanityConfig.version) {
972
+ console.warn('fetchSanity: The "version" property is missing in the config object.');
973
+ return false;
974
+ }
975
+ return true;
976
+ }