musora-content-services 2.102.0 → 2.102.2

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 CHANGED
@@ -2,6 +2,20 @@
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.102.2](https://github.com/railroadmedia/musora-content-services/compare/v2.102.1...v2.102.2) (2025-12-11)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **agi:** remove count from AGI lessons functions ([5cf3bc3](https://github.com/railroadmedia/musora-content-services/commit/5cf3bc3ade98faa5200cd64ce4b0a9edaff29d7a))
11
+
12
+ ### [2.102.1](https://github.com/railroadmedia/musora-content-services/compare/v2.102.0...v2.102.1) (2025-12-10)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **agi:** offset default and slug clause on artistBySlug ([10cc6d0](https://github.com/railroadmedia/musora-content-services/commit/10cc6d0390a12715aeba01c5dc72ecdb53d0f236))
18
+
5
19
  ## [2.102.0](https://github.com/railroadmedia/musora-content-services/compare/v2.101.1...v2.102.0) (2025-12-10)
6
20
 
7
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.102.0",
3
+ "version": "2.102.2",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -21,11 +21,12 @@
21
21
  },
22
22
  "homepage": "https://github.com/railroadmedia/musora-content-services#readme",
23
23
  "devDependencies": {
24
- "@babel/preset-env": "^7.25.3",
25
24
  "@babel/plugin-transform-class-properties": "^7.27.1",
26
25
  "@babel/plugin-transform-object-rest-spread": "^7.28.0",
26
+ "@babel/preset-env": "^7.25.3",
27
27
  "@babel/preset-typescript": "^7.27.1",
28
28
  "@sentry/browser": "^10.15.0",
29
+ "@types/jest": "^30.0.0",
29
30
  "babel-jest": "^29.7.0",
30
31
  "dotenv": "^16.4.5",
31
32
  "jest": "^29.7.0",
@@ -0,0 +1,460 @@
1
+ import { filtersToGroq } from '../../contentTypeConfig'
2
+ import { getPermissionsAdapter } from '../../services/permissions/index'
3
+ import type { UserPermissions } from '../../services/permissions/PermissionsAdapter'
4
+ import { filterOps } from './query'
5
+
6
+ // ============================================
7
+ // TYPES & INTERFACES
8
+ // ============================================
9
+ //
10
+
11
+ export type Prefix = '' | '@->' | '^.'
12
+
13
+ export interface PermissionsConfig {
14
+ bypassPermissions?: boolean
15
+ showMembershipRestrictedContent?: boolean
16
+ showOnlyOwnedContent?: boolean
17
+ userData?: UserPermissions
18
+ prefix?: Prefix
19
+ }
20
+
21
+ export interface StatusConfig {
22
+ statuses?: string[]
23
+ bypassStatuses?: boolean
24
+ isSingle?: boolean
25
+ isAdmin?: boolean
26
+ prefix?: Prefix
27
+ }
28
+
29
+ export interface DateConfig {
30
+ bypassPublishedDate?: boolean
31
+ pullFutureContent?: boolean
32
+ getFutureContentOnly?: boolean
33
+ prefix?: Prefix
34
+ }
35
+
36
+ export interface ContentFilterConfig extends PermissionsConfig, StatusConfig, DateConfig {
37
+ prefix?: Prefix
38
+ }
39
+
40
+ // ============================================
41
+ // CONSTANTS
42
+ // ============================================
43
+
44
+ const STATUS_SCHEDULED = 'scheduled'
45
+ const STATUS_PUBLISHED = 'published'
46
+ const STATUS_DRAFT = 'draft'
47
+ const STATUS_ARCHIVED = 'archived'
48
+ const STATUS_UNLISTED = 'unlisted'
49
+
50
+ const CHILD_PREFIX: Prefix = '@->'
51
+ const PARENT_PREFIX: Prefix = '^.'
52
+
53
+ // ============================================
54
+ // HELPER UTILITIES
55
+ // ============================================
56
+
57
+ const getRoundedTime = (): Date => {
58
+ const now = new Date()
59
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 1)
60
+ }
61
+
62
+ const applyPrefix = (prefix: Prefix, filter: string): string => {
63
+ if (!prefix || !filter) return filter
64
+ // Replace field names with prefixed versions
65
+ return filter.replace(/\b([a-z_][a-z0-9_]*)\s*(==|!=|<=|>=|<|>|in|match)/gi, `${prefix}$1 $2`)
66
+ }
67
+
68
+ const arrayToStringRepresentation = (arr: string[]): string => {
69
+ return '[' + arr.map((item) => `'${item}'`).join(',') + ']'
70
+ }
71
+
72
+ // ============================================
73
+ // MAIN FILTERS CLASS
74
+ // ============================================
75
+
76
+ export class Filters {
77
+ static empty = ''
78
+
79
+ // ============================================
80
+ // SIMPLE FILTERS (Synchronous)
81
+ // ============================================
82
+
83
+ /**
84
+ * @param {string} brand - The brand to filter by
85
+ * @returns {string} Filter expression
86
+ * @example Filters.brand('drumeo') // "brand == 'drumeo'"
87
+ */
88
+ static brand(brand: string): string {
89
+ return `brand == "${brand}"`
90
+ }
91
+
92
+ /**
93
+ * @param {string} type - The content type to filter by
94
+ * @returns {string} Filter expression
95
+ * @example Filters.type('song') // "_type == 'song'"
96
+ */
97
+ static type(type: string): string {
98
+ return `_type == "${type}"`
99
+ }
100
+
101
+ /**
102
+ * @param {string} slug - The slug to filter by
103
+ * @returns {string} Filter expression
104
+ * @example Filters.slug('guitar-basics') // "slug.current == 'guitar-basics'"
105
+ */
106
+ static slug(slug: string): string {
107
+ return `slug.current == "${slug}"`
108
+ }
109
+
110
+ /**
111
+ * @param {number} id - The railcontent_id to filter by
112
+ * @returns {string} Filter expression
113
+ * @example Filters.railcontentId(12345) // "railcontent_id == 12345"
114
+ */
115
+ static railcontentId(id: number): string {
116
+ return `railcontent_id == ${id}`
117
+ }
118
+
119
+ /**
120
+ * @param {string[]} statuses - Array of status values
121
+ * @returns {string} Filter expression
122
+ * @example Filters.statusIn(['published', 'scheduled']) // "status in ['published','scheduled']"
123
+ */
124
+ static statusIn(statuses: string[]): string {
125
+ return `status in ${arrayToStringRepresentation(statuses)}`
126
+ }
127
+
128
+ /**
129
+ * @param {number[]} ids - Array of railcontent_id values
130
+ * @returns {string} Filter expression
131
+ * @example Filters.idIn([123, 456, 789]) // "railcontent_id in [123,456,789]"
132
+ */
133
+ static idIn(ids: number[]): string {
134
+ return `railcontent_id in [${ids.join(',')}]`
135
+ }
136
+
137
+ /**
138
+ * @param {string} id - The document ID to reference
139
+ * @returns {string} Filter expression
140
+ * @example Filters.references('abc123') // "references('abc123')"
141
+ */
142
+ static references(id: string): string {
143
+ return `references("${id}")`
144
+ }
145
+
146
+ static referencesIDWithFilter(filter: string): string {
147
+ return `references(*[${filter}]._id)`
148
+ }
149
+
150
+ /**
151
+ * @returns {string} Filter expression
152
+ * @example Filters.referencesParent() // "references(^._id)"
153
+ */
154
+ static referencesParent(): string {
155
+ return `references(^._id)`
156
+ }
157
+
158
+ /**
159
+ * @param {string} field - The field to match in the reference query
160
+ * @param {string} value - The value to match
161
+ * @returns {string} Filter expression
162
+ * @example Filters.referencesField('slug.current', 'john-doe')
163
+ */
164
+ static referencesField(field: string, value: string): string {
165
+ return `references(*[${field} == "${value}"]._id)`
166
+ }
167
+
168
+ /**
169
+ * @param {string} term - The search term
170
+ * @returns {string} Filter expression
171
+ * @example Filters.titleMatch('guitar') // "title match 'guitar*'"
172
+ */
173
+ static titleMatch(term: string): string {
174
+ return `title match "${term}*"`
175
+ }
176
+
177
+ /**
178
+ * @param {string} field - The field to search in
179
+ * @param {string} term - The search term
180
+ * @returns {string} Filter expression
181
+ * @example Filters.searchMatch('description', 'beginner') // "description match 'beginner*'"
182
+ */
183
+ static searchMatch(field: string, term?: string): string {
184
+ return term ? `${field} match "${term}*"` : Filters.empty
185
+ }
186
+
187
+ /**
188
+ * @param {string} date - ISO date string
189
+ * @returns {string} Filter expression
190
+ * @example Filters.publishedBefore('2024-01-01') // "published_on <= '2024-01-01'"
191
+ */
192
+ static publishedBefore(date: string): string {
193
+ return `published_on <= "${date}"`
194
+ }
195
+
196
+ /**
197
+ * @param {string} date - ISO date string
198
+ * @returns {string} Filter expression
199
+ * @example Filters.publishedAfter('2024-01-01') // "published_on >= '2024-01-01'"
200
+ */
201
+ static publishedAfter(date: string): string {
202
+ return `published_on >= "${date}"`
203
+ }
204
+
205
+ /**
206
+ * @param {string} field - The field to check
207
+ * @returns {string} Filter expression
208
+ * @example Filters.defined('thumbnail') // "defined(thumbnail)"
209
+ */
210
+ static defined(field: string): string {
211
+ return `defined(${field})`
212
+ }
213
+
214
+ /**
215
+ * @param {string} field - The field to check
216
+ * @returns {string} Filter expression
217
+ * @example Filters.notDefined('thumbnail') // "!defined(thumbnail)"
218
+ */
219
+ static notDefined(field: string): string {
220
+ return `!defined(${field})`
221
+ }
222
+
223
+ /**
224
+ * @param {Prefix} [prefix=''] - Optional prefix for the field
225
+ * @returns {string} Filter expression
226
+ * @example Filters.notDeprecated() // "!defined(deprecated_railcontent_id)"
227
+ * @example Filters.notDeprecated('@->') // "!defined(@->deprecated_railcontent_id)"
228
+ */
229
+ static notDeprecated(prefix: Prefix = ''): string {
230
+ return `!defined(${prefix}deprecated_railcontent_id)`
231
+ }
232
+
233
+ // ============================================
234
+ // PREFIX MODIFIERS
235
+ // ============================================
236
+
237
+ /**
238
+ * @param {Prefix} prefix - The prefix to apply ('', '@->', '^.')
239
+ * @param {string} filter - The filter expression to prefix
240
+ * @returns {string} Filter expression with prefix applied
241
+ * @example Filters.withPrefix('@->', Filters.brand('drumeo'))
242
+ */
243
+ static withPrefix(prefix: Prefix, filter: string): string {
244
+ return applyPrefix(prefix, filter)
245
+ }
246
+
247
+ /**
248
+ * @param {string} filter - The filter expression to prefix
249
+ * @returns {string} Filter expression with child prefix (@->)
250
+ * @example Filters.asChild(Filters.statusIn(['published']))
251
+ */
252
+ static asChild(filter: string): string {
253
+ return applyPrefix(CHILD_PREFIX, filter)
254
+ }
255
+
256
+ /**
257
+ * @param {string} filter - The filter expression to prefix
258
+ * @returns {string} Filter expression with parent prefix (^.)
259
+ * @example Filters.asParent(Filters.brand('drumeo'))
260
+ */
261
+ static asParent(filter: string): string {
262
+ return applyPrefix(PARENT_PREFIX, filter)
263
+ }
264
+
265
+ // ============================================
266
+ // COMPOSITION UTILITIES
267
+ // ============================================
268
+
269
+ /**
270
+ * @param {...string} filters - Filter expressions to combine with AND
271
+ * @returns {string} Combined filter expression
272
+ * @example Filters.combine(Filters.brand('drumeo'), Filters.type('song'))
273
+ */
274
+ static combine(...filters: (string | undefined | null | false)[]): string {
275
+ return (filters.filter((f) => f) as string[]).reduce(filterOps.and.concat, filterOps.and.empty)
276
+ }
277
+
278
+ /**
279
+ * @param {...string} filters - Filter expressions to combine with OR
280
+ * @returns {string} Combined filter expression
281
+ * @example Filters.combineOr(Filters.type('song'), Filters.type('workout'))
282
+ */
283
+ static combineOr(...filters: (string | undefined | null | false)[]): string {
284
+ return (filters.filter((f) => f) as string[]).reduce(filterOps.or.concat, filterOps.or.empty)
285
+ }
286
+
287
+ /**
288
+ * @param {...(string | Promise<string>)} filters - Mix of synchronous filter expressions and promises
289
+ * @returns {Promise<string>} Promise that resolves to combined filter expression
290
+ * @example
291
+ * await Filters.combineAsync(
292
+ * Filters.brand('drumeo'),
293
+ * Filters.permissions({ bypassPermissions: false }),
294
+ * Filters.status({ isSingle: false })
295
+ * )
296
+ */
297
+ static async combineAsync(
298
+ ...filters: (string | Promise<string> | undefined | null | false)[]
299
+ ): Promise<string> {
300
+ const resolved = (await Promise.all(
301
+ filters.map((f) => Promise.resolve(f)).filter((f) => f)
302
+ )) as string[]
303
+
304
+ return resolved.reduce(filterOps.and.concat, filterOps.and.empty)
305
+ }
306
+
307
+ /**
308
+ * @param {...(string | Promise<string>)} filters - Mix of synchronous filter expressions and promises
309
+ * @returns {Promise<string>} Promise that resolves to combined filter expression
310
+ * @example
311
+ * await Filters.combineOrAsync(
312
+ * Filters.type('song'),
313
+ * Filters.type('workout'),
314
+ * Filters.permissions({ bypassPermissions: false })
315
+ * )
316
+ */
317
+ static async combineAsyncOr(
318
+ ...filters: (string | Promise<string> | undefined | null | false)[]
319
+ ): Promise<string> {
320
+ const resolved = (await Promise.all(
321
+ filters.map((f) => Promise.resolve(f)).filter((f) => f)
322
+ )) as string[]
323
+
324
+ return resolved.reduce(filterOps.or.concat, filterOps.or.empty)
325
+ }
326
+
327
+ // ============================================
328
+ // ASYNC FILTERS (Permission/Status based)
329
+ // ============================================
330
+
331
+ /**
332
+ * @param {PermissionsConfig} config - Permissions configuration
333
+ * @returns {Promise<string>} Filter expression based on user permissions
334
+ * @example await Filters.permissions({ bypassPermissions: false })
335
+ */
336
+ static async permissions(config: PermissionsConfig = {}): Promise<string> {
337
+ if (config.bypassPermissions) return ''
338
+
339
+ const adapter = getPermissionsAdapter()
340
+ const userData = config.userData || (await adapter.fetchUserPermissions())
341
+
342
+ if (adapter.isAdmin(userData)) return ''
343
+
344
+ const permissionsFilter = adapter.generatePermissionsFilter(userData, {
345
+ prefix: config.prefix || '',
346
+ showMembershipRestrictedContent: config.showMembershipRestrictedContent,
347
+ showOnlyOwnedContent: config.showOnlyOwnedContent,
348
+ })
349
+
350
+ return permissionsFilter || ''
351
+ }
352
+
353
+ /**
354
+ * @param {StatusConfig} config - Status configuration
355
+ * @returns {Promise<string>} Filter expression for status
356
+ * @example await Filters.status({ statuses: ['published', 'scheduled'] })
357
+ */
358
+ static async status(config: StatusConfig = {}): Promise<string> {
359
+ if (config.bypassStatuses) return ''
360
+
361
+ let statuses = config.statuses || []
362
+
363
+ // Auto-determine statuses if not provided
364
+ if (statuses.length === 0) {
365
+ const userData = await getPermissionsAdapter().fetchUserPermissions()
366
+ const isAdmin = getPermissionsAdapter().isAdmin(userData)
367
+
368
+ if (config.isAdmin || isAdmin) {
369
+ statuses = [
370
+ STATUS_DRAFT,
371
+ STATUS_SCHEDULED,
372
+ STATUS_PUBLISHED,
373
+ STATUS_ARCHIVED,
374
+ STATUS_UNLISTED,
375
+ ]
376
+ } else if (config.isSingle) {
377
+ statuses = [STATUS_SCHEDULED, STATUS_PUBLISHED, STATUS_UNLISTED, STATUS_ARCHIVED]
378
+ } else {
379
+ statuses = [STATUS_SCHEDULED, STATUS_PUBLISHED]
380
+ }
381
+ }
382
+
383
+ const filter = Filters.statusIn(statuses)
384
+ return config.prefix ? applyPrefix(config.prefix, filter) : filter
385
+ }
386
+
387
+ /**
388
+ * @param {DateConfig} config - Date configuration
389
+ * @returns {string} Filter expression for published date
390
+ * @example Filters.publishedDate({ pullFutureContent: false })
391
+ */
392
+ static publishedDate(config: DateConfig = {}): string {
393
+ if (config.bypassPublishedDate) return ''
394
+
395
+ const now = getRoundedTime().toISOString()
396
+
397
+ let filter = ''
398
+ if (config.getFutureContentOnly) {
399
+ filter = Filters.publishedAfter(now)
400
+ } else if (!config.pullFutureContent) {
401
+ filter = Filters.publishedBefore(now)
402
+ }
403
+
404
+ return config.prefix && filter ? applyPrefix(config.prefix, filter) : filter
405
+ }
406
+
407
+ // ============================================
408
+ // HIGH-LEVEL COMPOSITE FILTERS
409
+ // ============================================
410
+
411
+ /**
412
+ * @param {ContentFilterConfig} config - Complete filter configuration
413
+ * @returns {Promise<string>} Combined filter expression (status + permissions + date + deprecated)
414
+ * @example await Filters.contentFilter({ bypassPermissions: false, pullFutureContent: false })
415
+ */
416
+ static async contentFilter(config: ContentFilterConfig = {}): Promise<string> {
417
+ return Filters.combineAsync(
418
+ Filters.status(config),
419
+ Filters.permissions({ ...config }),
420
+ Filters.publishedDate(config),
421
+ Filters.notDeprecated(config.prefix || '')
422
+ )
423
+ }
424
+
425
+ /**
426
+ * @param {ContentFilterConfig} config - Complete filter configuration
427
+ * @returns {Promise<string>} Content filter with child prefix (@->)
428
+ * @example await Filters.childFilter({ showMembershipRestrictedContent: true })
429
+ */
430
+ static async childFilter(config: ContentFilterConfig = {}): Promise<string> {
431
+ return Filters.contentFilter({ ...config, prefix: CHILD_PREFIX })
432
+ }
433
+
434
+ /**
435
+ * @param {ContentFilterConfig} config - Complete filter configuration
436
+ * @returns {Promise<string>} Content filter with parent prefix (^.)
437
+ * @example await Filters.parentFilter({ bypassPermissions: true })
438
+ */
439
+ static async parentFilter(config: ContentFilterConfig = {}): Promise<string> {
440
+ return Filters.contentFilter({ ...config, prefix: PARENT_PREFIX })
441
+ }
442
+
443
+ // ============================================
444
+ // MISC UTILITIES FIlTERS
445
+ // ============================================
446
+ static includedFields(includedFields: string[]): string {
447
+ return includedFields.length > 0 ? filtersToGroq(includedFields) : Filters.empty
448
+ }
449
+
450
+ static count(filter: string): string {
451
+ return `count(*[${filter}])`
452
+ }
453
+
454
+ static progressIds(progressIds: number[]): string {
455
+ return progressIds.length > 0 ? Filters.idIn(progressIds) : Filters.empty
456
+ }
457
+ }
458
+
459
+ // Default export
460
+ export default Filters
@@ -20,7 +20,7 @@ export interface QueryBuilder {
20
20
  and(expr: string): QueryBuilder
21
21
  or(...exprs: string[]): QueryBuilder
22
22
  order(expr: string): QueryBuilder
23
- slice(start: number, end: number): QueryBuilder
23
+ slice(offset: number, limit?: number): QueryBuilder
24
24
  first(): QueryBuilder
25
25
  select(...fields: string[]): QueryBuilder
26
26
  postFilter(expr: string): QueryBuilder
@@ -54,6 +54,8 @@ const project: Monoid<string> = {
54
54
  concat: (a, b) => (!a ? b : !b ? a : `${a}, ${b}`),
55
55
  }
56
56
 
57
+ export const filterOps = { and, or }
58
+
57
59
  export const query = (): QueryBuilder => {
58
60
  let state: QueryBuilderState = {
59
61
  filter: and.empty,
@@ -83,8 +85,8 @@ export const query = (): QueryBuilder => {
83
85
  },
84
86
 
85
87
  // pagination / slicing
86
- slice(start: number = 0, end?: number) {
87
- const sliceExpr = !end ? `[${start}]` : `[${start}...${end}]`
88
+ slice(offset: number = 0, limit?: number) {
89
+ const sliceExpr = !limit ? `[${offset}]` : `[${offset}...${offset + limit}]`
88
90
 
89
91
  state.slice = slice.concat(state.slice, sliceExpr)
90
92
  return builder
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * @module Artist
3
3
  */
4
- import { filtersToGroq, getFieldsForContentType } from '../../contentTypeConfig.js'
5
- import { FilterBuilder } from '../../filterBuilder.js'
4
+ import { getFieldsForContentType } from '../../contentTypeConfig.js'
6
5
  import { BuildQueryOptions, query } from '../../lib/sanity/query'
7
6
  import { fetchSanity, getSortOrder } from '../sanity.js'
8
7
  import { Lesson } from './content'
9
8
  import { Brands } from '../../lib/brands'
9
+ import { Filters as f } from '../../lib/sanity/filter'
10
10
 
11
11
  export interface Artist {
12
12
  slug: string
@@ -35,27 +35,28 @@ export async function fetchArtists(
35
35
  brand: Brands | string,
36
36
  options: BuildQueryOptions = { sort: 'lower(name) asc' }
37
37
  ): Promise<Artists> {
38
- const lessonFilter = await new FilterBuilder(`brand == "${brand}" && references(^._id)`, {
39
- bypassPermissions: true,
40
- }).buildFilter()
38
+ const lessonFilter = f.combine(f.brand(brand), f.referencesParent())
39
+ const type = f.type('artist')
40
+ const lessonCount = `count(*[${lessonFilter}])`
41
+ const postFilter = `lessonCount > 0`
41
42
 
42
43
  const data = query()
43
- .and(`_type == "artist"`)
44
+ .and(type)
44
45
  .order(options?.sort || 'lower(name) asc')
45
- .slice(options?.offset || 0, (options?.offset || 0) + (options?.limit || 20))
46
+ .slice(options?.offset || 0, options?.limit || 20)
46
47
  .select(
47
48
  'name',
48
49
  `"slug": slug.current`,
49
50
  `"thumbnail": thumbnail_url.asset->url`,
50
- `"lessonCount": count(*[${lessonFilter}])`
51
+ `"lessonCount": ${lessonCount}`
51
52
  )
52
- .postFilter(`lessonCount > 0`)
53
+ .postFilter(postFilter)
53
54
  .build()
54
55
 
55
56
  const total = query()
56
- .and(`_type == "artist"`)
57
- .select(`"lessonCount": count(*[${lessonFilter}])`)
58
- .postFilter(`lessonCount > 0`)
57
+ .and(type)
58
+ .select(`"lessonCount": ${lessonCount}`)
59
+ .postFilter(postFilter)
59
60
  .build()
60
61
 
61
62
  const q = `{
@@ -82,14 +83,11 @@ export async function fetchArtistBySlug(
82
83
  slug: string,
83
84
  brand?: Brands | string
84
85
  ): Promise<Artist | null> {
85
- const brandFilter = brand ? `brand == "${brand}" && ` : ''
86
- const filter = await new FilterBuilder(`${brandFilter} _type == "song" && references(^._id)`, {
87
- bypassPermissions: true,
88
- }).buildFilter()
86
+ const filter = f.combine(brand ? f.brand(brand) : f.empty, f.referencesParent())
89
87
 
90
88
  const q = query()
91
- .and(`_type == "artist"`)
92
- .and(`&& slug.current == '${slug}'`)
89
+ .and(f.type('artist'))
90
+ .and(f.slug(slug))
93
91
  .select(
94
92
  'name',
95
93
  `"slug": slug.current`,
@@ -139,31 +137,31 @@ export async function fetchArtistLessons(
139
137
  {
140
138
  sort = '-published_on',
141
139
  searchTerm = '',
142
- offset = 1,
140
+ offset = 0,
143
141
  limit = 10,
144
142
  includedFields = [],
145
143
  progressIds = [],
146
144
  }: ArtistLessonOptions = {}
147
145
  ): Promise<ArtistLessons> {
148
- const fieldsString = getFieldsForContentType(contentType) as string
149
- const searchFilter = searchTerm ? `&& title match "${searchTerm}*"` : ''
150
- const includedFieldsFilter = includedFields.length > 0 ? filtersToGroq(includedFields) : ''
151
- const addType = contentType ? `_type == '${contentType}' && ` : ''
152
- const progressFilter =
153
- progressIds.length > 0 ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
154
- const filter = `${addType} brand == '${brand}' ${searchFilter} ${includedFieldsFilter} && references(*[_type=='artist' && slug.current == '${slug}']._id) ${progressFilter}`
155
- const filterWithRestrictions = await new FilterBuilder(filter).buildFilter()
156
-
157
146
  sort = getSortOrder(sort, brand)
158
147
 
148
+ const restrictions = await f.combineAsync(
149
+ f.contentFilter(),
150
+ f.referencesIDWithFilter(f.combine(f.type('artist'), f.slug(slug)))
151
+ )
152
+
159
153
  const data = query()
160
- .and(filterWithRestrictions)
154
+ .and(f.brand(brand))
155
+ .and(f.searchMatch('title', searchTerm))
156
+ .and(f.includedFields(includedFields))
157
+ .and(f.progressIds(progressIds))
158
+ .and(restrictions)
161
159
  .order(sort)
162
- .slice(offset, offset + limit)
163
- .select(...(fieldsString ? [fieldsString] : []))
160
+ .slice(offset, limit)
161
+ .select(getFieldsForContentType(contentType) as string)
164
162
  .build()
165
163
 
166
- const total = query().and(filterWithRestrictions).build()
164
+ const total = query().and(restrictions).build()
167
165
 
168
166
  const q = `{
169
167
  "data": ${data},