musora-content-services 2.102.1 → 2.102.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 +14 -0
- package/package.json +3 -2
- package/src/lib/sanity/filter.ts +460 -0
- package/src/lib/sanity/query.ts +5 -3
- package/src/services/content/artist.ts +29 -31
- package/src/services/content/genre.ts +30 -31
- package/src/services/content/instructor.ts +28 -28
- package/test/lib/__snapshots__/filter.test.ts.snap +5 -0
- package/test/lib/filter.test.ts +1148 -0
- package/test/lib/{query.test.js → query.test.ts} +3 -3
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.3](https://github.com/railroadmedia/musora-content-services/compare/v2.102.2...v2.102.3) (2025-12-11)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **agi:** rollback total on fetchAGILessons ([a02be05](https://github.com/railroadmedia/musora-content-services/commit/a02be0592dd8b195da7971f13ea062674510c8bb))
|
|
11
|
+
|
|
12
|
+
### [2.102.2](https://github.com/railroadmedia/musora-content-services/compare/v2.102.1...v2.102.2) (2025-12-11)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **agi:** remove count from AGI lessons functions ([5cf3bc3](https://github.com/railroadmedia/musora-content-services/commit/5cf3bc3ade98faa5200cd64ce4b0a9edaff29d7a))
|
|
18
|
+
|
|
5
19
|
### [2.102.1](https://github.com/railroadmedia/musora-content-services/compare/v2.102.0...v2.102.1) (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.
|
|
3
|
+
"version": "2.102.3",
|
|
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
|
package/src/lib/sanity/query.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
87
|
-
const sliceExpr = !
|
|
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 {
|
|
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 =
|
|
39
|
-
|
|
40
|
-
})
|
|
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(
|
|
44
|
+
.and(type)
|
|
44
45
|
.order(options?.sort || 'lower(name) asc')
|
|
45
|
-
.slice(options?.offset || 0,
|
|
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":
|
|
51
|
+
`"lessonCount": ${lessonCount}`
|
|
51
52
|
)
|
|
52
|
-
.postFilter(
|
|
53
|
+
.postFilter(postFilter)
|
|
53
54
|
.build()
|
|
54
55
|
|
|
55
56
|
const total = query()
|
|
56
|
-
.and(
|
|
57
|
-
.select(`"lessonCount":
|
|
58
|
-
.postFilter(
|
|
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
|
|
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(
|
|
92
|
-
.and(
|
|
89
|
+
.and(f.type('artist'))
|
|
90
|
+
.and(f.slug(slug))
|
|
93
91
|
.select(
|
|
94
92
|
'name',
|
|
95
93
|
`"slug": slug.current`,
|
|
@@ -145,25 +143,25 @@ export async function fetchArtistLessons(
|
|
|
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(
|
|
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,
|
|
163
|
-
.select(
|
|
160
|
+
.slice(offset, limit)
|
|
161
|
+
.select(getFieldsForContentType(contentType) as string)
|
|
164
162
|
.build()
|
|
165
163
|
|
|
166
|
-
const total = query().and(
|
|
164
|
+
const total = query().and(restrictions).build()
|
|
167
165
|
|
|
168
166
|
const q = `{
|
|
169
167
|
"data": ${data},
|