musora-content-services 2.95.1 → 2.95.3
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 +9 -0
- package/package.json +1 -1
- package/src/lib/ads/monoid.ts +5 -0
- package/src/lib/ads/semigroup.ts +3 -0
- package/src/lib/sanity/query.ts +125 -28
- package/src/services/content/artist.ts +69 -37
- package/src/services/content/genre.ts +67 -39
- package/src/services/content/instructor.ts +76 -41
- package/src/services/sync/fetch.ts +7 -1
- package/.claude/settings.local.json +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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.95.3](https://github.com/railroadmedia/musora-content-services/compare/v2.95.2...v2.95.3) (2025-12-08)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* make agi responses standard ([#630](https://github.com/railroadmedia/musora-content-services/issues/630)) ([0301133](https://github.com/railroadmedia/musora-content-services/commit/03011331cb835412af80111a97e6ae83fd6e56d5))
|
|
11
|
+
|
|
12
|
+
### [2.95.2](https://github.com/railroadmedia/musora-content-services/compare/v2.95.1...v2.95.2) (2025-12-08)
|
|
13
|
+
|
|
5
14
|
### [2.95.1](https://github.com/railroadmedia/musora-content-services/compare/v2.95.0...v2.95.1) (2025-12-08)
|
|
6
15
|
|
|
7
16
|
## [2.95.0](https://github.com/railroadmedia/musora-content-services/compare/v2.94.8...v2.95.0) (2025-12-08)
|
package/package.json
CHANGED
package/src/lib/sanity/query.ts
CHANGED
|
@@ -1,30 +1,127 @@
|
|
|
1
|
+
import { Monoid } from '../ads/monoid'
|
|
2
|
+
import { Semigroup } from '../ads/semigroup'
|
|
3
|
+
|
|
1
4
|
export interface BuildQueryOptions {
|
|
2
|
-
sort
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
filter: string
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
sort?: string
|
|
6
|
+
offset?: number
|
|
7
|
+
limit?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Projection = string[]
|
|
11
|
+
|
|
12
|
+
export interface QueryBuilderState {
|
|
13
|
+
filter: string
|
|
14
|
+
ordering: string
|
|
15
|
+
slice: string
|
|
16
|
+
projection: string
|
|
17
|
+
postFilter: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface QueryBuilder {
|
|
21
|
+
and(expr: string): QueryBuilder
|
|
22
|
+
or(...exprs: string[]): QueryBuilder
|
|
23
|
+
order(expr: string): QueryBuilder
|
|
24
|
+
slice(start: number, end: number): QueryBuilder
|
|
25
|
+
first(): QueryBuilder
|
|
26
|
+
select(...fields: string[]): QueryBuilder
|
|
27
|
+
postFilter(expr: string): QueryBuilder
|
|
28
|
+
build(): string
|
|
29
|
+
|
|
30
|
+
_state(): QueryBuilderState
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const and: Monoid<string> = {
|
|
34
|
+
empty: '',
|
|
35
|
+
concat: (a, b) => (!a ? b : !b ? a : `${a} && ${b}`),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const or: Monoid<string> = {
|
|
39
|
+
empty: '',
|
|
40
|
+
concat: (a, b) => (!a ? b : !b ? a : `(${a} || ${b})`),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const order: Monoid<string> = {
|
|
44
|
+
empty: '',
|
|
45
|
+
concat: (a, b) => `| order(${b || a})`,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const slice: Monoid<string> = {
|
|
49
|
+
empty: '',
|
|
50
|
+
concat: (a, b) => b || a,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const project: Semigroup<string> = {
|
|
54
|
+
concat: (a, b) => `${a}, ${b}`,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const query = (): QueryBuilder => {
|
|
58
|
+
let state: QueryBuilderState = {
|
|
59
|
+
filter: and.empty,
|
|
60
|
+
ordering: order.empty,
|
|
61
|
+
slice: slice.empty,
|
|
62
|
+
projection: '_id',
|
|
63
|
+
postFilter: and.empty,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const builder: QueryBuilder = {
|
|
67
|
+
// main filters
|
|
68
|
+
and(expr: string) {
|
|
69
|
+
state.filter = and.concat(state.filter, expr)
|
|
70
|
+
return builder
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
or(...exprs: string[]) {
|
|
74
|
+
const orExpr = exprs.reduce(or.concat, or.empty)
|
|
75
|
+
state.filter = and.concat(state.filter, orExpr)
|
|
76
|
+
return builder
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// sorting
|
|
80
|
+
order(expr: string) {
|
|
81
|
+
state.ordering = order.concat(state.ordering, expr)
|
|
82
|
+
return builder
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// pagination / slicing
|
|
86
|
+
slice(start: number = 0, end?: number) {
|
|
87
|
+
const sliceExpr = !end ? `[${start}]` : `[${start}...${end}]`
|
|
88
|
+
|
|
89
|
+
state.slice = slice.concat(state.slice, sliceExpr)
|
|
90
|
+
return builder
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
first() {
|
|
94
|
+
return this.slice()
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// projection
|
|
98
|
+
select(...fields: string[]) {
|
|
99
|
+
state.projection = fields.reduce(project.concat, state.projection)
|
|
100
|
+
return builder
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// post filters
|
|
104
|
+
postFilter(expr: string) {
|
|
105
|
+
state.postFilter = and.concat(state.postFilter, expr)
|
|
106
|
+
return builder
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
build() {
|
|
110
|
+
const { filter, ordering, slice, projection } = state
|
|
111
|
+
|
|
112
|
+
return `
|
|
113
|
+
*[${filter}]
|
|
114
|
+
${ordering}
|
|
115
|
+
${slice}
|
|
116
|
+
{ ${projection} }
|
|
117
|
+
${state.postFilter ? `[${state.postFilter}]` : ''}
|
|
118
|
+
`.trim()
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
_state() {
|
|
122
|
+
return state
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return builder
|
|
30
127
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.js'
|
|
5
5
|
import { FilterBuilder } from '../../filterBuilder.js'
|
|
6
|
-
import {
|
|
6
|
+
import { BuildQueryOptions, query } from '../../lib/sanity/query'
|
|
7
7
|
import { fetchSanity, getSortOrder } from '../sanity.js'
|
|
8
8
|
import { Lesson } from './content'
|
|
9
9
|
import { Brands } from '../../lib/brands'
|
|
@@ -26,19 +26,39 @@ export interface Artist {
|
|
|
26
26
|
* .then(artists => console.log(artists))
|
|
27
27
|
* .catch(error => console.error(error));
|
|
28
28
|
*/
|
|
29
|
-
export async function fetchArtists(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
export async function fetchArtists(
|
|
30
|
+
brand: Brands | string,
|
|
31
|
+
options: BuildQueryOptions = { sort: 'lower(name) asc' }
|
|
32
|
+
): Promise<Artist[] | null> {
|
|
33
|
+
const lessonFilter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
|
|
34
|
+
bypassPermissions: true,
|
|
35
|
+
}).buildFilter()
|
|
36
|
+
|
|
37
|
+
const data = query()
|
|
38
|
+
.and(`_type == "artist"`)
|
|
39
|
+
.order(options.sort || 'lower(name) asc')
|
|
40
|
+
.slice(options.offset || 0, (options.offset || 0) + (options.limit || 20))
|
|
41
|
+
.select(
|
|
42
|
+
'name',
|
|
43
|
+
`"slug": slug.current`,
|
|
44
|
+
`"thumbnail": thumbnail_url.asset->url`,
|
|
45
|
+
`"lessonCount": count(*[${lessonFilter}])`
|
|
46
|
+
)
|
|
47
|
+
.postFilter(`lessonCount > 0`)
|
|
48
|
+
.build()
|
|
49
|
+
|
|
50
|
+
const total = query()
|
|
51
|
+
.and(`_type == "artist"`)
|
|
52
|
+
.select(`"lessonCount": count(*[${lessonFilter}])`)
|
|
53
|
+
.postFilter(`lessonCount > 0`)
|
|
54
|
+
.build()
|
|
55
|
+
|
|
56
|
+
const q = `{
|
|
57
|
+
"data": ${data},
|
|
58
|
+
"total": ${total}
|
|
59
|
+
}`
|
|
60
|
+
|
|
61
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
/**
|
|
@@ -61,25 +81,29 @@ export async function fetchArtistBySlug(
|
|
|
61
81
|
const filter = await new FilterBuilder(`${brandFilter} _type == "song" && references(^._id)`, {
|
|
62
82
|
bypassPermissions: true,
|
|
63
83
|
}).buildFilter()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
|
|
85
|
+
const q = query()
|
|
86
|
+
.and(`_type == "artist"`)
|
|
87
|
+
.and(`&& slug.current == '${slug}'`)
|
|
88
|
+
.select(
|
|
89
|
+
'name',
|
|
90
|
+
`"slug": slug.current`,
|
|
91
|
+
`"thumbnail": thumbnail_url.asset->url`,
|
|
92
|
+
`"lessonCount": count(*[${filter}])`
|
|
93
|
+
)
|
|
94
|
+
.first()
|
|
95
|
+
.build()
|
|
96
|
+
|
|
97
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
export interface ArtistLessonOptions {
|
|
74
|
-
sort?: string
|
|
100
|
+
export interface ArtistLessonOptions extends BuildQueryOptions {
|
|
75
101
|
searchTerm?: string
|
|
76
|
-
page?: number
|
|
77
|
-
limit?: number
|
|
78
102
|
includedFields?: Array<string>
|
|
79
103
|
progressIds?: Array<number>
|
|
80
104
|
}
|
|
81
105
|
|
|
82
|
-
export interface
|
|
106
|
+
export interface ArtistLessons {
|
|
83
107
|
data: Lesson[]
|
|
84
108
|
total: number
|
|
85
109
|
}
|
|
@@ -96,7 +120,7 @@ export interface LessonsByArtistResponse {
|
|
|
96
120
|
* @param {number} [params.limit=10] - The number of items per page.
|
|
97
121
|
* @param {Array<string>} [params.includedFields=[]] - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock'].
|
|
98
122
|
* @param {Array<number>} [params.progressId=[]] - The ids of the lessons that are in progress or completed
|
|
99
|
-
* @returns {Promise<
|
|
123
|
+
* @returns {Promise<ArtistLessons>} - The lessons for the artist
|
|
100
124
|
*
|
|
101
125
|
* @example
|
|
102
126
|
* fetchArtistLessons('10 Years', 'drumeo', 'song', {'-published_on', '', 1, 10, ["difficulty,Intermediate"], [232168, 232824, 303375, 232194, 393125]})
|
|
@@ -110,15 +134,13 @@ export async function fetchArtistLessons(
|
|
|
110
134
|
{
|
|
111
135
|
sort = '-published_on',
|
|
112
136
|
searchTerm = '',
|
|
113
|
-
|
|
137
|
+
offset = 1,
|
|
114
138
|
limit = 10,
|
|
115
139
|
includedFields = [],
|
|
116
140
|
progressIds = [],
|
|
117
141
|
}: ArtistLessonOptions = {}
|
|
118
|
-
): Promise<
|
|
142
|
+
): Promise<ArtistLessons> {
|
|
119
143
|
const fieldsString = getFieldsForContentType(contentType) as string
|
|
120
|
-
const start = (page - 1) * limit
|
|
121
|
-
const end = start + limit
|
|
122
144
|
const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
|
|
123
145
|
const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
|
|
124
146
|
const addType = contentType ? `_type == '${contentType}' && ` : ''
|
|
@@ -128,10 +150,20 @@ export async function fetchArtistLessons(
|
|
|
128
150
|
const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
|
|
129
151
|
|
|
130
152
|
sort = getSortOrder(sort, brand)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
|
|
154
|
+
const data = query()
|
|
155
|
+
.and(filterWithRestrictions)
|
|
156
|
+
.order(sort)
|
|
157
|
+
.slice(offset, offset + limit)
|
|
158
|
+
.select(...(fieldsString ? [fieldsString] : []))
|
|
159
|
+
.build()
|
|
160
|
+
|
|
161
|
+
const total = query().and(filterWithRestrictions).build()
|
|
162
|
+
|
|
163
|
+
const q = `{
|
|
164
|
+
"data": ${data},
|
|
165
|
+
"total": ${total}
|
|
166
|
+
}`
|
|
167
|
+
|
|
168
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
137
169
|
}
|
|
@@ -5,7 +5,7 @@ import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.
|
|
|
5
5
|
import { fetchSanity, getSortOrder } from '../sanity.js'
|
|
6
6
|
import { FilterBuilder } from '../../filterBuilder.js'
|
|
7
7
|
import { Lesson } from './content'
|
|
8
|
-
import {
|
|
8
|
+
import { BuildQueryOptions, query } from '../../lib/sanity/query'
|
|
9
9
|
import { Brands } from '../../lib/brands'
|
|
10
10
|
|
|
11
11
|
export interface Genre {
|
|
@@ -26,20 +26,39 @@ export interface Genre {
|
|
|
26
26
|
* .then(genres => console.log(genres))
|
|
27
27
|
* .catch(error => console.error(error));
|
|
28
28
|
*/
|
|
29
|
-
export async function fetchGenres(
|
|
30
|
-
|
|
29
|
+
export async function fetchGenres(
|
|
30
|
+
brand: Brands | string,
|
|
31
|
+
options: BuildQueryOptions = { sort: 'lower(name) asc' }
|
|
32
|
+
): Promise<Genre[]> {
|
|
33
|
+
const lessonFilter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
|
|
31
34
|
bypassPermissions: true,
|
|
32
35
|
}).buildFilter()
|
|
33
36
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const data = query()
|
|
38
|
+
.and(`_type == "genre"`)
|
|
39
|
+
.order(options.sort || 'lower(name) asc')
|
|
40
|
+
.slice(options.offset || 0, (options.offset || 0) + (options.limit || 20))
|
|
41
|
+
.select(
|
|
42
|
+
'name',
|
|
43
|
+
`"slug": slug.current`,
|
|
44
|
+
`"thumbnail": thumbnail_url.asset->url`,
|
|
45
|
+
`"lessons_count": count(*[${lessonFilter}])`
|
|
46
|
+
)
|
|
47
|
+
.postFilter(`lessons_count > 0`)
|
|
48
|
+
.build()
|
|
49
|
+
|
|
50
|
+
const total = query()
|
|
51
|
+
.and(`_type == "genre"`)
|
|
52
|
+
.select(`"lessons_count": count(*[${lessonFilter}])`)
|
|
53
|
+
.postFilter(`lessons_count > 0`)
|
|
54
|
+
.build()
|
|
55
|
+
|
|
56
|
+
const q = `{
|
|
57
|
+
"data": ${data},
|
|
58
|
+
"total": ${total}
|
|
59
|
+
}`
|
|
60
|
+
|
|
61
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/**
|
|
@@ -47,7 +66,7 @@ export async function fetchGenres(brand: Brands | string): Promise<Genre[]> {
|
|
|
47
66
|
*
|
|
48
67
|
* @param {string} slug - The slug of the genre to fetch.
|
|
49
68
|
* @param {Brands|string} [brand] - The brand for which to fetch the genre. Lesson count will be filtered by this brand if provided.
|
|
50
|
-
* @returns {Promise<Genre
|
|
69
|
+
* @returns {Promise<Genre | null>} - A promise that resolves to an genre object or null if not found.
|
|
51
70
|
*
|
|
52
71
|
* @example
|
|
53
72
|
* fetchGenreBySlug('drumeo')
|
|
@@ -63,27 +82,28 @@ export async function fetchGenreBySlug(
|
|
|
63
82
|
bypassPermissions: true,
|
|
64
83
|
}).buildFilter()
|
|
65
84
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
const q = query()
|
|
86
|
+
.and(`_type == "genre"`)
|
|
87
|
+
.and(`slug.current == "${slug}"`)
|
|
88
|
+
.select(
|
|
89
|
+
'name',
|
|
90
|
+
`"slug": slug.current`,
|
|
91
|
+
`"thumbnail": thumbnail_url.asset->url`,
|
|
92
|
+
`"lessons_count": count(*[${filter}])`
|
|
93
|
+
)
|
|
94
|
+
.first()
|
|
95
|
+
.build()
|
|
96
|
+
|
|
97
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
export interface
|
|
78
|
-
sort?: string
|
|
100
|
+
export interface GenreLessonsOptions extends BuildQueryOptions {
|
|
79
101
|
searchTerm?: string
|
|
80
|
-
page?: number
|
|
81
|
-
limit?: number
|
|
82
102
|
includedFields?: Array<string>
|
|
83
103
|
progressIds?: Array<number>
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
export interface
|
|
106
|
+
export interface GenreLessons {
|
|
87
107
|
data: Lesson[]
|
|
88
108
|
total: number
|
|
89
109
|
}
|
|
@@ -99,7 +119,7 @@ export interface LessonsByGenreResponse {
|
|
|
99
119
|
* @param {number} [params.limit=10] - The number of items per page.
|
|
100
120
|
* @param {Array<string>} [params.includedFields=[]] - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock'].
|
|
101
121
|
* @param {Array<number>} [params.progressIds=[]] - The ids of the lessons that are in progress or completed
|
|
102
|
-
* @returns {Promise<
|
|
122
|
+
* @returns {Promise<GenreLessons|null>} - The lessons for the genre
|
|
103
123
|
*
|
|
104
124
|
* @example
|
|
105
125
|
* fetchGenreLessons('Blues', 'drumeo', 'song', {'-published_on', '', 1, 10, ["difficulty,Intermediate"], [232168, 232824, 303375, 232194, 393125]})
|
|
@@ -113,15 +133,13 @@ export async function fetchGenreLessons(
|
|
|
113
133
|
{
|
|
114
134
|
sort = '-published_on',
|
|
115
135
|
searchTerm = '',
|
|
116
|
-
|
|
136
|
+
offset = 1,
|
|
117
137
|
limit = 10,
|
|
118
138
|
includedFields = [],
|
|
119
139
|
progressIds = [],
|
|
120
|
-
}:
|
|
121
|
-
): Promise<
|
|
140
|
+
}: GenreLessonsOptions = {}
|
|
141
|
+
): Promise<GenreLessons> {
|
|
122
142
|
const fieldsString = getFieldsForContentType(contentType) as string
|
|
123
|
-
const start = (page - 1) * limit
|
|
124
|
-
const end = start + limit
|
|
125
143
|
const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
|
|
126
144
|
const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
|
|
127
145
|
const addType = contentType ? `_type == '${contentType}' && ` : ''
|
|
@@ -131,10 +149,20 @@ export async function fetchGenreLessons(
|
|
|
131
149
|
const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
|
|
132
150
|
|
|
133
151
|
sort = getSortOrder(sort, brand)
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
const data = query()
|
|
153
|
+
.and(filterWithRestrictions)
|
|
154
|
+
.and(`brand == ${brand}`)
|
|
155
|
+
.order(sort)
|
|
156
|
+
.slice(offset, offset + limit)
|
|
157
|
+
.select(...(fieldsString ? [fieldsString] : []))
|
|
158
|
+
.build()
|
|
159
|
+
|
|
160
|
+
const total = query().and(filterWithRestrictions).build()
|
|
161
|
+
|
|
162
|
+
const q = `{
|
|
163
|
+
"data": ${data},
|
|
164
|
+
"total": ${total}
|
|
165
|
+
}`
|
|
166
|
+
|
|
167
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
140
168
|
}
|
|
@@ -5,7 +5,7 @@ import { FilterBuilder } from '../../filterBuilder.js'
|
|
|
5
5
|
import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.js'
|
|
6
6
|
import { fetchSanity, getSortOrder } from '../sanity.js'
|
|
7
7
|
import { Lesson } from './content'
|
|
8
|
-
import {
|
|
8
|
+
import { BuildQueryOptions, query } from '../../lib/sanity/query'
|
|
9
9
|
import { Brands } from '../../lib/brands'
|
|
10
10
|
|
|
11
11
|
export interface Instructor {
|
|
@@ -16,30 +16,55 @@ export interface Instructor {
|
|
|
16
16
|
thumbnail: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface Instructors {
|
|
20
|
+
data: Instructor[]
|
|
21
|
+
total: number
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* Fetch all instructor with lessons available for a specific brand.
|
|
21
26
|
*
|
|
22
27
|
* @param {Brands|string} brand - The brand for which to fetch instructors.
|
|
23
|
-
* @returns {Promise<
|
|
28
|
+
* @returns {Promise<Instructors>} - A promise that resolves to an array of instructor objects.
|
|
24
29
|
*
|
|
25
30
|
* @example
|
|
26
31
|
* fetchInstructors('drumeo')
|
|
27
32
|
* .then(instructors => console.log(instructors))
|
|
28
33
|
* .catch(error => console.error(error));
|
|
29
34
|
*/
|
|
30
|
-
export async function fetchInstructors(
|
|
31
|
-
|
|
35
|
+
export async function fetchInstructors(
|
|
36
|
+
brand: Brands | string,
|
|
37
|
+
options: BuildQueryOptions
|
|
38
|
+
): Promise<Instructor[]> {
|
|
39
|
+
const lessonFilter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
|
|
32
40
|
bypassPermissions: true,
|
|
33
41
|
}).buildFilter()
|
|
34
42
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
name
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
const data = query()
|
|
44
|
+
.and(`_type == "instructor"`)
|
|
45
|
+
.order(options.sort || 'lower(name) asc')
|
|
46
|
+
.slice(options.offset || 0, (options.offset || 0) + (options.limit || 20))
|
|
47
|
+
.select(
|
|
48
|
+
'name',
|
|
49
|
+
`"slug": slug.current`,
|
|
50
|
+
`"thumbnail": thumbnail_url.asset->url`,
|
|
51
|
+
`"lessonCount": count(*[${lessonFilter}])`
|
|
52
|
+
)
|
|
53
|
+
.postFilter(`lessonCount > 0`)
|
|
54
|
+
.build()
|
|
55
|
+
|
|
56
|
+
const total = query()
|
|
57
|
+
.and(`_type == "instructor"`)
|
|
58
|
+
.select(`"lessonCount": count(*[${lessonFilter}])`)
|
|
59
|
+
.postFilter(`lessonCount > 0`)
|
|
60
|
+
.build()
|
|
61
|
+
|
|
62
|
+
const q = `{
|
|
63
|
+
"data": ${data},
|
|
64
|
+
"total": count(${total})
|
|
65
|
+
}`
|
|
66
|
+
|
|
67
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
/**
|
|
@@ -47,7 +72,7 @@ export async function fetchInstructors(brand: Brands | string): Promise<Instruct
|
|
|
47
72
|
*
|
|
48
73
|
* @param {string} slug - The slug of the instructor to fetch.
|
|
49
74
|
* @param {Brands|string} [brand] - The brand for which to fetch the instructor. Lesson count will be filtered by this brand if provided.
|
|
50
|
-
* @returns {Promise<Instructor
|
|
75
|
+
* @returns {Promise<Instructor | null>} - A promise that resolves to an instructor object or null if not found.
|
|
51
76
|
*
|
|
52
77
|
* @example
|
|
53
78
|
* fetchInstructorBySlug('66samus', 'drumeo')
|
|
@@ -63,26 +88,28 @@ export async function fetchInstructorBySlug(
|
|
|
63
88
|
bypassPermissions: true,
|
|
64
89
|
}).buildFilter()
|
|
65
90
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
const q = query()
|
|
92
|
+
.and(`_type == "instructor"`)
|
|
93
|
+
.and(`slug.current == "${slug}"`)
|
|
94
|
+
.select(
|
|
95
|
+
'name',
|
|
96
|
+
`"slug": slug.current`,
|
|
97
|
+
'short_bio',
|
|
98
|
+
`"thumbnail": thumbnail_url.asset->url`,
|
|
99
|
+
`"lessonCount": count(*[${filter}])`
|
|
100
|
+
)
|
|
101
|
+
.first()
|
|
102
|
+
.build()
|
|
103
|
+
|
|
104
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
75
105
|
}
|
|
76
106
|
|
|
77
|
-
export interface
|
|
78
|
-
sortOrder?: string
|
|
107
|
+
export interface InstructorLessonsOptions extends BuildQueryOptions {
|
|
79
108
|
searchTerm?: string
|
|
80
|
-
page?: number
|
|
81
|
-
limit?: number
|
|
82
109
|
includedFields?: Array<string>
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
export interface
|
|
112
|
+
export interface InstructorLessons {
|
|
86
113
|
data: Lesson[]
|
|
87
114
|
total: number
|
|
88
115
|
}
|
|
@@ -99,7 +126,7 @@ export interface InstructorLessonsResponse {
|
|
|
99
126
|
* @param {number} [options.limit=10] - The number of items per page.
|
|
100
127
|
* @param {Array<string>} [options.includedFields=[]] - Additional filters to apply to the query in the format of a key,value array. eg. ['difficulty,Intermediate', 'genre,rock'].
|
|
101
128
|
*
|
|
102
|
-
* @returns {Promise<
|
|
129
|
+
* @returns {Promise<InstructorLessons>} - The lessons for the instructor or null if not found.
|
|
103
130
|
* @example
|
|
104
131
|
* fetchInstructorLessons('instructor123')
|
|
105
132
|
* .then(lessons => console.log(lessons))
|
|
@@ -109,26 +136,34 @@ export async function fetchInstructorLessons(
|
|
|
109
136
|
slug: string,
|
|
110
137
|
brand: Brands | string,
|
|
111
138
|
{
|
|
112
|
-
|
|
139
|
+
sort = '-published_on',
|
|
113
140
|
searchTerm = '',
|
|
114
|
-
|
|
141
|
+
offset = 1,
|
|
115
142
|
limit = 20,
|
|
116
143
|
includedFields = [],
|
|
117
|
-
}:
|
|
118
|
-
): Promise<
|
|
144
|
+
}: InstructorLessonsOptions = {}
|
|
145
|
+
): Promise<InstructorLessons> {
|
|
119
146
|
const fieldsString = getFieldsForContentType() as string
|
|
120
|
-
const start = (page - 1) * limit
|
|
121
|
-
const end = start + limit
|
|
122
147
|
const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
|
|
123
148
|
const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
|
|
124
149
|
const filter = `brand == '${brand}' ${searchFilter} ${includedFieldsFilter} && references(*[_type=='instructor' && slug.current == '${slug}']._id)`
|
|
125
150
|
const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
|
|
126
151
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
sort = getSortOrder(sort, brand)
|
|
153
|
+
|
|
154
|
+
const data = query()
|
|
155
|
+
.and(filterWithRestrictions)
|
|
156
|
+
.order(sort)
|
|
157
|
+
.slice(offset, offset + limit)
|
|
158
|
+
.select(...(fieldsString ? [fieldsString] : []))
|
|
159
|
+
.build()
|
|
160
|
+
|
|
161
|
+
const total = query().and(filterWithRestrictions).build()
|
|
162
|
+
|
|
163
|
+
const q = `{
|
|
164
|
+
"data": ${data},
|
|
165
|
+
"total": count(${total})
|
|
166
|
+
}`
|
|
167
|
+
|
|
168
|
+
return fetchSanity(q, true, { processNeedAccess: false, processPageType: false })
|
|
134
169
|
}
|
|
@@ -308,7 +308,13 @@ function serializeIds(ids: { id: RecordId }): { client_record_id: RecordId } {
|
|
|
308
308
|
|
|
309
309
|
function deserializeRecord(record: SyncSyncable<BaseModel, 'client_record_id'> | null): SyncSyncable<BaseModel, 'id'> | null {
|
|
310
310
|
if (record) {
|
|
311
|
-
const { client_record_id: id, ...rest } = record
|
|
311
|
+
const { client_record_id: id, ...rest } = record as any
|
|
312
|
+
|
|
313
|
+
if ('collection_type' in rest && rest.collection_type === 'self') {
|
|
314
|
+
rest.collection_type = null
|
|
315
|
+
rest.collection_id = null
|
|
316
|
+
}
|
|
317
|
+
|
|
312
318
|
return {
|
|
313
319
|
...rest,
|
|
314
320
|
id
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(find:*)",
|
|
5
|
-
"Bash(docker exec:*)",
|
|
6
|
-
"Bash(npm test:*)",
|
|
7
|
-
"WebSearch",
|
|
8
|
-
"WebFetch(domain:watermelondb.dev)",
|
|
9
|
-
"WebFetch(domain:github.com)",
|
|
10
|
-
"Bash(git checkout:*)",
|
|
11
|
-
"Bash(npm run doc:*)",
|
|
12
|
-
"Bash(cat:*)",
|
|
13
|
-
"Bash(tr:*)"
|
|
14
|
-
],
|
|
15
|
-
"deny": [],
|
|
16
|
-
"ask": []
|
|
17
|
-
}
|
|
18
|
-
}
|