musora-content-services 2.185.1 → 2.186.1

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.
@@ -123,6 +123,15 @@ export class Filters {
123
123
  return `slug.current == "${escapeDoubleQuoted(slug)}"`
124
124
  }
125
125
 
126
+ /**
127
+ * @param {string[]} slugs - Array of slugs to filter by
128
+ * @returns {string} Filter expression
129
+ * @example Filters.slugIn(['a', 'b']) // "slug.current in ['a','b']"
130
+ */
131
+ static slugIn(slugs: string[]): string {
132
+ return `slug.current in ${arrayToStringRepresentation(slugs)}`
133
+ }
134
+
126
135
  /**
127
136
  * @param {number} id - The railcontent_id to filter by
128
137
  * @returns {string} Filter expression
@@ -218,6 +227,59 @@ export class Filters {
218
227
  return `published_on >= "${escapeDoubleQuoted(date)}"`
219
228
  }
220
229
 
230
+ /**
231
+ * @param {string} field - The date field to compare
232
+ * @param {string} date - ISO date string
233
+ * @returns {string} Filter expression
234
+ * @example Filters.dateBefore('ends_at', '2026-01-01') // 'ends_at <= "2026-01-01"'
235
+ */
236
+ static dateBefore(field: string, date: string): string {
237
+ return `${field} <= "${escapeDoubleQuoted(date)}"`
238
+ }
239
+
240
+ /**
241
+ * @param {string} field - The date field to compare
242
+ * @param {string} date - ISO date string
243
+ * @returns {string} Filter expression
244
+ * @example Filters.dateAfter('starts_at', '2026-01-01') // 'starts_at >= "2026-01-01"'
245
+ */
246
+ static dateAfter(field: string, date: string): string {
247
+ return `${field} >= "${escapeDoubleQuoted(date)}"`
248
+ }
249
+
250
+ /**
251
+ * Matches documents whose window covers the given date.
252
+ *
253
+ * @param {string} startField - The field holding the start of the window
254
+ * @param {string} endField - The field holding the end of the window
255
+ * @param {string} date - ISO date string
256
+ * @returns {string} Filter expression
257
+ * @example Filters.dateWithin('starts_at', 'ends_at', '2026-11-25')
258
+ */
259
+ static dateWithin(startField: string, endField: string, date: string): string {
260
+ return Filters.combine(Filters.dateBefore(startField, date), Filters.dateAfter(endField, date))
261
+ }
262
+
263
+ /**
264
+ * @param {string} field - The boolean field to check
265
+ * @param {boolean} [value=true] - The value to match
266
+ * @returns {string} Filter expression
267
+ * @example Filters.flag('active') // "active == true"
268
+ */
269
+ static flag(field: string, value: boolean = true): string {
270
+ return `${field} == ${value}`
271
+ }
272
+
273
+ /**
274
+ * @param {string} field - The field to compare
275
+ * @param {string} value - The value to match
276
+ * @returns {string} Filter expression
277
+ * @example Filters.fieldEquals('key', 'upgrade-banner') // 'key == "upgrade-banner"'
278
+ */
279
+ static fieldEquals(field: string, value: string): string {
280
+ return `${field} == "${escapeDoubleQuoted(value)}"`
281
+ }
282
+
221
283
  /**
222
284
  * @param {string} field - The field to check
223
285
  * @returns {string} Filter expression
@@ -1,30 +1,54 @@
1
- import { Either } from '../ads/either'
1
+ import { AsyncEither } from '../ads/async-either'
2
2
  import { FieldAccess } from './field-access'
3
- import { composite, query, QueryBuilder } from './query'
3
+ import { composite, query, QueryBuilder, QueryBuilderState } from './query'
4
4
  import { QueryRunner, run, SanityQueryError } from './runner'
5
5
 
6
+ type ElementOf<T> = T extends (infer U)[] ? U : T
7
+
8
+ type Field<T> = (keyof ElementOf<T> & string) | (string & {})
9
+
6
10
  export interface RunnableQuery {
7
11
  build(): string
8
12
  toString(): string
9
- run<T>(runner?: QueryRunner<T>): Promise<Either<SanityQueryError, T | null>>
13
+ run<T>(runner?: QueryRunner<T>): AsyncEither<SanityQueryError, T | null>
10
14
  }
11
15
 
12
- export interface GroqBuilder extends QueryBuilder, RunnableQuery {
13
- selector(selector: string): GroqBuilder
14
- and(expr: string): GroqBuilder
15
- or(...exprs: string[]): GroqBuilder
16
- order(expr: string): GroqBuilder
17
- slice(offset: number, limit?: number): GroqBuilder
18
- first(): GroqBuilder
19
- select(...fields: string[]): GroqBuilder
20
- access(field: string, fieldAccess?: FieldAccess): GroqBuilder
21
- dereference(): GroqBuilder
22
- postFilter(expr: string): GroqBuilder
16
+ interface Chainable<T, Self> {
17
+ selector(selector: string): Self
18
+ and(expr: string): Self
19
+ or(...exprs: string[]): Self
20
+ order(expr: string): Self
21
+ slice(offset: number, limit?: number): Self
22
+ select(...fields: Field<T>[]): Self
23
+ access(field: Field<T>, fieldAccess?: FieldAccess): Self
24
+ dereference(): Self
25
+ postFilter(expr: string): Self
26
+ build(): string
27
+ toString(): string
28
+
29
+ _state(): QueryBuilderState
30
+ }
31
+
32
+ /**
33
+ * `Empty` is what `run` resolves to when the query matched nothing. {@link GroqBuilder.default}
34
+ * supplies a fallback and narrows it to `never`, which is the state a builder is assumed to be in
35
+ * unless it says otherwise: a builder that has not been given one is a `GroqBuilder<T, null>`.
36
+ */
37
+ export interface GroqBuilder<T = unknown, Empty = never> extends Chainable<T, GroqBuilder<T, Empty>> {
38
+ /**
39
+ * Never read. Methods are checked bivariantly, so without a property here a builder with no
40
+ * fallback would silently satisfy a signature promising one.
41
+ */
42
+ readonly __empty?: Empty
43
+
44
+ first(): GroqBuilder<ElementOf<T>, Empty>
45
+ default(value: T): GroqBuilder<T, never>
46
+ run<U = T>(runner?: QueryRunner<U>): AsyncEither<SanityQueryError, U | Empty>
23
47
  }
24
48
 
25
49
  /**
26
50
  * @param {string} selector
27
- * @returns {GroqBuilder}
51
+ * @returns {GroqBuilder<T>}
28
52
  * @example
29
53
  * const result = await groq()
30
54
  * .and(f.type('song'))
@@ -32,13 +56,30 @@ export interface GroqBuilder extends QueryBuilder, RunnableQuery {
32
56
  * .slice(0, 10)
33
57
  * .select('_id', 'title')
34
58
  * .run<Song[]>()
59
+ * @example
60
+ * // naming the type up front keeps it off every call in the chain
61
+ * const songs = await groq<Song[]>().and(f.type('song')).default([]).run()
35
62
  */
36
- const groqBuilder = (selector?: string): GroqBuilder => {
63
+ const groqBuilder = <T = unknown>(selector?: string): GroqBuilder<T, null> => {
37
64
  const builder = query(selector)
65
+ let fallback: { value: unknown } | undefined
66
+
67
+ const runnable = Object.assign(builder, {
68
+ default: (value: unknown) => {
69
+ fallback = { value }
70
+ return runnable
71
+ },
72
+ run: <U = T>(runner?: QueryRunner<U>) => {
73
+ const result = run<U>(builder.build(), runner)
74
+ const fallbackAtRunTime = fallback
75
+
76
+ return fallbackAtRunTime
77
+ ? result.map((value) => value ?? (fallbackAtRunTime.value as U))
78
+ : result
79
+ },
80
+ })
38
81
 
39
- return Object.assign(builder, {
40
- run: <T>(runner?: QueryRunner<T>) => run<T>(builder.build(), runner),
41
- }) as GroqBuilder
82
+ return runnable as unknown as GroqBuilder<T, null>
42
83
  }
43
84
 
44
85
  /**
@@ -1,3 +1,4 @@
1
+ import { AsyncEither } from '../ads/async-either'
1
2
  import { Either } from '../ads/either'
2
3
  import { SanityClient } from '../../infrastructure/sanity/SanityClient'
3
4
 
@@ -44,7 +45,7 @@ const defaultRunner = <T>(): QueryRunner<T> =>
44
45
  /**
45
46
  * @param {string} groq
46
47
  * @param {QueryRunner<T>} runner
47
- * @returns {Promise<Either<SanityQueryError, T | null>>}
48
+ * @returns {AsyncEither<SanityQueryError, T | null>}
48
49
  * @example
49
50
  * const result = await run<Song[]>(query().and(f.type('song')).build())
50
51
  * result.fold(
@@ -61,4 +62,4 @@ const defaultRunner = <T>(): QueryRunner<T> =>
61
62
  export const run = <T>(
62
63
  groq: string,
63
64
  runner: QueryRunner<T> = defaultRunner<T>()
64
- ): Promise<Either<SanityQueryError, T | null>> => runner(groq)
65
+ ): AsyncEither<SanityQueryError, T | null> => AsyncEither.of(runner(groq))
@@ -92,3 +92,36 @@ export type FaqDocument = {
92
92
  answer?: string
93
93
  }[]
94
94
  }
95
+
96
+ export type PromotionalCampaignDocument = {
97
+ _id: string
98
+ _type: "promotional-campaign"
99
+ _createdAt: string
100
+ _updatedAt: string
101
+ _rev: string
102
+ /** Campaign Name */
103
+ name: string
104
+ slug: { _type: "slug"; current: string }
105
+ /** Starts At */
106
+ starts_at: string
107
+ /** Ends At */
108
+ ends_at: string
109
+ /** Active? */
110
+ active?: boolean
111
+ /** Placements */
112
+ placements: {
113
+ _key: string
114
+ /** Placement Key */
115
+ key: string
116
+ title?: string
117
+ body?: string
118
+ /** Resolved to a CDN url by the promotions query projection. */
119
+ dark_mode_image?: string
120
+ /** Resolved to a CDN url by the promotions query projection. */
121
+ light_mode_image?: string
122
+ /** CTA Label */
123
+ cta_label?: string
124
+ /** Dismissible? */
125
+ dismissible?: boolean
126
+ }[]
127
+ }
@@ -132,6 +132,10 @@ class AwardDefinitionsService {
132
132
 
133
133
  const awards = await fetchSanity(query, true, { processNeedAccess: false, processPageType: false })
134
134
 
135
+ if (!awards) {
136
+ return
137
+ }
138
+
135
139
  this.definitions.clear()
136
140
  this.contentIndex.clear()
137
141