musora-content-services 2.167.2 → 2.167.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/.agent/decisions/2026-07-20-mu2-1724-songs-lessons-counts.md +33 -0
- package/.agent/decisions/2026-07-20-query-builder-tostring.md +37 -0
- package/CHANGELOG.md +8 -0
- package/package.json +2 -1
- package/src/contentTypeConfig.js +1 -0
- package/src/lib/sanity/query.ts +5 -0
- package/src/services/config.js +15 -0
- package/src/services/content/artist.ts +1 -3
- package/src/services/content/counts.ts +47 -0
- package/src/services/content/genre.ts +1 -3
- package/src/services/content/instructor.ts +1 -3
- package/src/services/userActivity.js +2 -0
- package/src/version-info.js +1 -0
- package/.claude/settings.local.json +0 -53
- package/test/initializeTests.js +0 -60
- package/test/integration/awards/award-exclusion-handling.test.ts +0 -275
- package/test/integration/awards/internal/image-utils.test.ts +0 -86
- package/test/integration/content-progress/__mocks__/mocks.ts +0 -104
- package/test/integration/content-progress/contentProgress.test.ts +0 -335
- package/test/integration/content-progress/e2eOfflineProgress.test.ts +0 -352
- package/test/integration/content-progress/e2eProgress.test.ts +0 -612
- package/test/integration/content-progress/getters.test.ts +0 -334
- package/test/integration/content-progress/helpers.test.ts +0 -263
- package/test/integration/content-progress/offlineContentProgress.test.ts +0 -226
- package/test/integration/forums.test.ts +0 -209
- package/test/integration/infrastructure/FetchRequestExecutor.test.ts +0 -88
- package/test/integration/initializeTestDB.ts +0 -80
- package/test/integration/learning-paths.test.ts +0 -854
- package/test/integration/notifications.test.ts +0 -178
- package/test/integration/progress-row/content-card.test.ts +0 -283
- package/test/integration/sync/adapters/idb-errors.test.ts +0 -144
- package/test/integration/sync/adapters/sqlite-errors.test.ts +0 -173
- package/test/integration/sync/fetch.test.ts +0 -224
- package/test/integration/sync/repositories/content-likes.test.ts +0 -99
- package/test/integration/sync/repositories/practices.test.ts +0 -179
- package/test/integration/sync/repositories/progress.test.ts +0 -245
- package/test/integration/sync/repositories/user-award-progress.static.test.ts +0 -68
- package/test/integration/sync/repositories/user-award-progress.test.ts +0 -387
- package/test/integration/sync/store/cross-user-protection.test.ts +0 -217
- package/test/integration/sync/store/store-idb.test.ts +0 -180
- package/test/integration/sync/store/store.test.ts +0 -361
- package/test/integration/user/permissions.test.js +0 -20
- package/test/integration/userActivity.test.ts +0 -102
- package/test/live/README.md +0 -29
- package/test/live/content.test.js +0 -116
- package/test/localStorageMock.js +0 -21
- package/test/log.js +0 -5
- package/test/mockData/award-definitions.js +0 -171
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -35
- package/test/mockData/mockData_progress_content.json +0 -182
- package/test/mockData/mockData_sanity_progress_content.json +0 -1451
- package/test/mockData/mockData_user_practices.json +0 -9
- package/test/setupConsole.js +0 -6
- package/test/setupNetworkGuard.js +0 -3
- package/test/setupTimers.js +0 -13
- package/test/unit/HttpClient.test.ts +0 -267
- package/test/unit/awards/award-alacarte-observer.test.ts +0 -199
- package/test/unit/awards/award-auto-refresh.test.ts +0 -84
- package/test/unit/awards/award-calculations.test.ts +0 -34
- package/test/unit/awards/award-callbacks.test.ts +0 -144
- package/test/unit/awards/award-certificate-display.test.ts +0 -330
- package/test/unit/awards/award-collection-edge-cases.test.ts +0 -212
- package/test/unit/awards/award-collection-filtering.test.ts +0 -287
- package/test/unit/awards/award-completion-flow.test.ts +0 -306
- package/test/unit/awards/award-multi-lesson.test.ts +0 -243
- package/test/unit/awards/award-observer-integration.test.ts +0 -327
- package/test/unit/awards/award-query-messages.test.ts +0 -448
- package/test/unit/awards/award-user-collection.test.ts +0 -416
- package/test/unit/awards/duplicate-prevention.test.ts +0 -120
- package/test/unit/awards/helpers/completion-mock.ts +0 -56
- package/test/unit/awards/helpers/index.ts +0 -3
- package/test/unit/awards/helpers/mock-setup.ts +0 -79
- package/test/unit/awards/helpers/progress-emitter.ts +0 -45
- package/test/unit/awards/message-generator.test.ts +0 -162
- package/test/unit/content-progress/bubbleTrickle.test.ts +0 -322
- package/test/unit/content-progress/helpers.test.ts +0 -329
- package/test/unit/content-progress/navigateTo.test.ts +0 -392
- package/test/unit/contentLikes.test.ts +0 -62
- package/test/unit/contentMetaData.test.ts +0 -58
- package/test/unit/dataContext.test.ts +0 -38
- package/test/unit/dateUtils.test.ts +0 -188
- package/test/unit/endScreen.test.js +0 -712
- package/test/unit/eventsAPI.test.ts +0 -159
- package/test/unit/imageSRCBuilder.test.ts +0 -37
- package/test/unit/imageSRCVerify.test.ts +0 -155
- package/test/unit/infrastructure/DefaultHeaderProvider.test.ts +0 -39
- package/test/unit/infrastructure/sanity/ContentClient.test.ts +0 -168
- package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +0 -93
- package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +0 -174
- package/test/unit/infrastructure/sanity/SanityClient.test.ts +0 -140
- package/test/unit/lib/__snapshots__/filter.test.ts.snap +0 -5
- package/test/unit/lib/filter.test.ts +0 -1154
- package/test/unit/lib/lastUpdated.test.ts +0 -21
- package/test/unit/lib/query.test.ts +0 -1469
- package/test/unit/lib/sanity/decorators/base.test.ts +0 -368
- package/test/unit/lib/sanity/decorators/navigate-to.test.ts +0 -361
- package/test/unit/lib/sanity/decorators/need-access.test.ts +0 -89
- package/test/unit/lib/sanity/decorators/need-lifetime-upgrade.test.ts +0 -85
- package/test/unit/lib/sanity/decorators/page-type.test.ts +0 -81
- package/test/unit/playerSettings.test.ts +0 -73
- package/test/unit/progress-row/content-card.test.ts +0 -314
- package/test/unit/progress-row/playlist-card.test.ts +0 -104
- package/test/unit/progressRows.test.ts +0 -243
- package/test/unit/recommendations.test.js +0 -47
- package/test/unit/sanityQueryService.test.ts +0 -200
- package/test/unit/search.test.ts +0 -44
- package/test/unit/sentry.test.ts +0 -62
- package/test/unit/services/progress-internal/bubble.test.ts +0 -279
- package/test/unit/services/progress.test.ts +0 -396
- package/test/unit/streakMessage.test.ts +0 -251
- package/test/unit/sync/context.test.ts +0 -51
- package/test/unit/sync/effects/logout-warning.test.ts +0 -158
- package/test/unit/sync/effects/push-failure-notification.test.ts +0 -196
- package/test/unit/sync/errors/sync-errors.test.ts +0 -106
- package/test/unit/sync/errors/validators.test.ts +0 -61
- package/test/unit/sync/helpers/TestModel.ts +0 -44
- package/test/unit/sync/helpers/index.ts +0 -172
- package/test/unit/sync/manager.test.ts +0 -303
- package/test/unit/sync/models/user-award-progress.test.ts +0 -82
- package/test/unit/sync/resolver.test.ts +0 -229
- package/test/unit/sync/retry.test.ts +0 -314
- package/test/unit/sync/run-scope.test.ts +0 -23
- package/test/unit/sync/stale-record-cleanup.test.ts +0 -107
- package/test/unit/sync/store/push-coalescer.test.ts +0 -156
- package/test/unit/sync/store-configs.test.ts +0 -37
- package/test/unit/sync/telemetry/sync-telemetry.test.ts +0 -118
- package/test/unit/sync/utils/event-emitter.test.ts +0 -64
- package/test/unit/sync/utils/throttle.test.ts +0 -245
- package/test/unit/url-builder.test.ts +0 -72
- package/test/unit/user/account.test.ts +0 -66
- package/test/unit/user/permissions.test.js +0 -191
|
@@ -1,1469 +0,0 @@
|
|
|
1
|
-
import { query } from '../../../src/lib/sanity/query'
|
|
2
|
-
|
|
3
|
-
describe('Sanity Query Builder', () => {
|
|
4
|
-
describe('Basic Query Building', () => {
|
|
5
|
-
test('builds empty query', () => {
|
|
6
|
-
const result = query().build()
|
|
7
|
-
expect(result).toBe('*[]')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
test('builds query with single filter', () => {
|
|
11
|
-
const result = query().and('_type == "course"').build()
|
|
12
|
-
expect(result).toBe('*[_type == "course"]')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('builds query with multiple and filters', () => {
|
|
16
|
-
const result = query().and('_type == "course"').and('brand == "drumeo"').build()
|
|
17
|
-
expect(result).toBe('*[_type == "course" && brand == "drumeo"]')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('builds query with three and filters', () => {
|
|
21
|
-
const result = query()
|
|
22
|
-
.and('_type == "course"')
|
|
23
|
-
.and('brand == "drumeo"')
|
|
24
|
-
.and('published == true')
|
|
25
|
-
.build()
|
|
26
|
-
expect(result).toBe('*[_type == "course" && brand == "drumeo" && published == true]')
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
describe('OR Filters', () => {
|
|
31
|
-
test('builds query with single or expression', () => {
|
|
32
|
-
const result = query().or('brand == "drumeo"', 'brand == "pianote"').build()
|
|
33
|
-
expect(result).toBe('*[(brand == "drumeo" || brand == "pianote")]')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('builds query with multiple or expressions', () => {
|
|
37
|
-
const result = query()
|
|
38
|
-
.or('brand == "drumeo"', 'brand == "pianote"', 'brand == "guitareo"')
|
|
39
|
-
.build()
|
|
40
|
-
expect(result).toBe('*[((brand == "drumeo" || brand == "pianote") || brand == "guitareo")]')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('combines and with or filters', () => {
|
|
44
|
-
const result = query()
|
|
45
|
-
.and('_type == "course"')
|
|
46
|
-
.or('brand == "drumeo"', 'brand == "pianote"')
|
|
47
|
-
.build()
|
|
48
|
-
expect(result).toBe('*[_type == "course" && (brand == "drumeo" || brand == "pianote")]')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('combines multiple and with or filters', () => {
|
|
52
|
-
const result = query()
|
|
53
|
-
.and('_type == "course"')
|
|
54
|
-
.and('published == true')
|
|
55
|
-
.or('brand == "drumeo"', 'brand == "pianote"')
|
|
56
|
-
.build()
|
|
57
|
-
expect(result).toBe(
|
|
58
|
-
'*[_type == "course" && published == true && (brand == "drumeo" || brand == "pianote")]'
|
|
59
|
-
)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('handles single or expression', () => {
|
|
63
|
-
const result = query().or('brand == "drumeo"').build()
|
|
64
|
-
expect(result).toBe('*[brand == "drumeo"]')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('handles empty or expressions', () => {
|
|
68
|
-
const result = query().or().build()
|
|
69
|
-
expect(result).toBe('*[]')
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('Ordering', () => {
|
|
74
|
-
test('builds query with order', () => {
|
|
75
|
-
const result = query().and('_type == "course"').order('publishedOn desc').build()
|
|
76
|
-
expect(result).toContain('*[_type == "course"]')
|
|
77
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('builds query with order only', () => {
|
|
81
|
-
const result = query().order('title asc').build()
|
|
82
|
-
expect(result).toContain('*[]')
|
|
83
|
-
expect(result).toContain('| order(title asc)')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('overrides previous order when called multiple times', () => {
|
|
87
|
-
const result = query()
|
|
88
|
-
.and('_type == "course"')
|
|
89
|
-
.order('publishedOn desc')
|
|
90
|
-
.order('title asc')
|
|
91
|
-
.build()
|
|
92
|
-
expect(result).toContain('*[_type == "course"]')
|
|
93
|
-
expect(result).toContain('| order(title asc)')
|
|
94
|
-
expect(result).not.toContain('publishedOn desc')
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('Slicing and Pagination', () => {
|
|
99
|
-
test('builds query with range slice', () => {
|
|
100
|
-
const result = query().and('_type == "course"').slice(0, 10).build()
|
|
101
|
-
expect(result).toContain('*[_type == "course"]')
|
|
102
|
-
expect(result).toContain('[0...10]')
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test('builds query with single index slice', () => {
|
|
106
|
-
const result = query().and('_type == "course"').slice(5).build()
|
|
107
|
-
expect(result).toContain('*[_type == "course"]')
|
|
108
|
-
expect(result).toContain('[5]')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('builds query with first() helper', () => {
|
|
112
|
-
const result = query().and('_type == "course"').first().build()
|
|
113
|
-
expect(result).toContain('*[_type == "course"]')
|
|
114
|
-
expect(result).toContain('[0]')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test('overrides previous slice when called multiple times', () => {
|
|
118
|
-
const result = query().and('_type == "course"').slice(0, 10).slice(10, 10).build()
|
|
119
|
-
expect(result).toContain('*[_type == "course"]')
|
|
120
|
-
expect(result).toContain('[10...20]')
|
|
121
|
-
expect(result).not.toContain('[0...10]')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
test('handles slice with zero start', () => {
|
|
125
|
-
const result = query().slice(0, 5).build()
|
|
126
|
-
expect(result).toContain('[0...5]')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
describe('Projection (Select)', () => {
|
|
131
|
-
test('builds query with single field selection', () => {
|
|
132
|
-
const result = query().and('_type == "course"').select('_id').build()
|
|
133
|
-
expect(result).toContain('*[_type == "course"]')
|
|
134
|
-
expect(result).toContain('{ _id }')
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
test('builds query with multiple field selections', () => {
|
|
138
|
-
const result = query().and('_type == "course"').select('_id', 'title', 'brand').build()
|
|
139
|
-
expect(result).toContain('*[_type == "course"]')
|
|
140
|
-
expect(result).toContain('{ _id, title, brand }')
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
test('builds query with select called multiple times', () => {
|
|
144
|
-
const result = query()
|
|
145
|
-
.and('_type == "course"')
|
|
146
|
-
.select('_id')
|
|
147
|
-
.select('title')
|
|
148
|
-
.select('brand')
|
|
149
|
-
.build()
|
|
150
|
-
expect(result).toContain('*[_type == "course"]')
|
|
151
|
-
expect(result).toContain('{ _id, title, brand }')
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
test('builds query with select only', () => {
|
|
155
|
-
const result = query().select('_id', 'title').build()
|
|
156
|
-
expect(result).toContain('*[]')
|
|
157
|
-
expect(result).toContain('{ _id, title }')
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test('handles empty select', () => {
|
|
161
|
-
const result = query().and('_type == "course"').select().build()
|
|
162
|
-
expect(result).toBe('*[_type == "course"]')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
test('handles complex projection with nested fields', () => {
|
|
166
|
-
const result = query()
|
|
167
|
-
.and('_type == "course"')
|
|
168
|
-
.select('_id', '"instructor": instructor->name', '"lessons": lessons[]->title')
|
|
169
|
-
.build()
|
|
170
|
-
expect(result).toContain('*[_type == "course"]')
|
|
171
|
-
expect(result).toContain(
|
|
172
|
-
'{ _id, "instructor": instructor->name, "lessons": lessons[]->title }'
|
|
173
|
-
)
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe('Post Filters', () => {
|
|
178
|
-
test('builds query with post filter', () => {
|
|
179
|
-
const result = query()
|
|
180
|
-
.and('_type == "course"')
|
|
181
|
-
.select('_id', 'title', '"lessonCount": count(lessons)')
|
|
182
|
-
.postFilter('lessonCount > 5')
|
|
183
|
-
.build()
|
|
184
|
-
expect(result).toContain('*[_type == "course"]')
|
|
185
|
-
expect(result).toContain('{ _id, title, "lessonCount": count(lessons) }')
|
|
186
|
-
expect(result).toContain('[lessonCount > 5]')
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test('builds query with multiple post filters', () => {
|
|
190
|
-
const result = query()
|
|
191
|
-
.and('_type == "course"')
|
|
192
|
-
.select('_id', 'title', '"lessonCount": count(lessons)')
|
|
193
|
-
.postFilter('lessonCount > 5')
|
|
194
|
-
.postFilter('lessonCount < 20')
|
|
195
|
-
.build()
|
|
196
|
-
expect(result).toContain('*[_type == "course"]')
|
|
197
|
-
expect(result).toContain('{ _id, title, "lessonCount": count(lessons) }')
|
|
198
|
-
expect(result).toContain('[lessonCount > 5 && lessonCount < 20]')
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
test('builds query with post filter only', () => {
|
|
202
|
-
const result = query()
|
|
203
|
-
.select('_id', '"lessonCount": count(lessons)')
|
|
204
|
-
.postFilter('lessonCount > 5')
|
|
205
|
-
.build()
|
|
206
|
-
expect(result).toContain('*[]')
|
|
207
|
-
expect(result).toContain('{ _id, "lessonCount": count(lessons) }')
|
|
208
|
-
expect(result).toContain('[lessonCount > 5]')
|
|
209
|
-
})
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
describe('Complex Query Combinations', () => {
|
|
213
|
-
test('builds query with all features combined', () => {
|
|
214
|
-
const result = query()
|
|
215
|
-
.and('_type == "course"')
|
|
216
|
-
.and('published == true')
|
|
217
|
-
.or('brand == "drumeo"', 'brand == "pianote"')
|
|
218
|
-
.select('_id', 'title', '"lessonCount": count(lessons)')
|
|
219
|
-
.postFilter('lessonCount > 5')
|
|
220
|
-
.order('publishedOn desc')
|
|
221
|
-
.slice(0, 10)
|
|
222
|
-
.build()
|
|
223
|
-
expect(result).toContain(
|
|
224
|
-
'*[_type == "course" && published == true && (brand == "drumeo" || brand == "pianote")]'
|
|
225
|
-
)
|
|
226
|
-
expect(result).toContain('{ _id, title, "lessonCount": count(lessons) }')
|
|
227
|
-
expect(result).toContain('[lessonCount > 5]')
|
|
228
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
229
|
-
expect(result).toContain('[0...10]')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('builds query with order and slice only', () => {
|
|
233
|
-
const result = query().order('publishedOn desc').slice(0, 10).build()
|
|
234
|
-
expect(result).toContain('*[]')
|
|
235
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
236
|
-
expect(result).toContain('[0...10]')
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
test('builds query with filter, order, and first', () => {
|
|
240
|
-
const result = query()
|
|
241
|
-
.and('_type == "course"')
|
|
242
|
-
.and('brand == "drumeo"')
|
|
243
|
-
.order('publishedOn desc')
|
|
244
|
-
.first()
|
|
245
|
-
.build()
|
|
246
|
-
expect(result).toContain('*[_type == "course" && brand == "drumeo"]')
|
|
247
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
248
|
-
expect(result).toContain('[0]')
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
test('builds realistic content query', () => {
|
|
252
|
-
const result = query()
|
|
253
|
-
.and('_type in ["course", "play-along", "song"]')
|
|
254
|
-
.and('!(_id in path("drafts.**"))')
|
|
255
|
-
.and('published == true')
|
|
256
|
-
.or('difficulty == "beginner"', 'difficulty == "intermediate"')
|
|
257
|
-
.select('_id', 'title', 'brand', 'difficulty', 'publishedOn')
|
|
258
|
-
.order('publishedOn desc')
|
|
259
|
-
.slice(0, 20)
|
|
260
|
-
.build()
|
|
261
|
-
expect(result).toContain('*[_type in ["course", "play-along", "song"]')
|
|
262
|
-
expect(result).toContain('!(_id in path("drafts.**"))')
|
|
263
|
-
expect(result).toContain('published == true')
|
|
264
|
-
expect(result).toContain('(difficulty == "beginner" || difficulty == "intermediate")')
|
|
265
|
-
expect(result).toContain('{ _id, title, brand, difficulty, publishedOn }')
|
|
266
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
267
|
-
expect(result).toContain('[0...20]')
|
|
268
|
-
})
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
describe('Method Chaining', () => {
|
|
272
|
-
test('returns same builder instance for chaining', () => {
|
|
273
|
-
const builder = query()
|
|
274
|
-
const result1 = builder.and('_type == "course"')
|
|
275
|
-
const result2 = result1.order('title')
|
|
276
|
-
const result3 = result2.slice(0, 10)
|
|
277
|
-
expect(result1).toBe(builder)
|
|
278
|
-
expect(result2).toBe(builder)
|
|
279
|
-
expect(result3).toBe(builder)
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
test('maintains state across chained calls', () => {
|
|
283
|
-
const builder = query()
|
|
284
|
-
builder.and('_type == "course"')
|
|
285
|
-
builder.and('brand == "drumeo"')
|
|
286
|
-
builder.order('publishedOn desc')
|
|
287
|
-
const result = builder.build()
|
|
288
|
-
expect(result).toContain('*[_type == "course" && brand == "drumeo"]')
|
|
289
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
290
|
-
})
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
describe('State Management', () => {
|
|
294
|
-
test('exposes internal state via _state()', () => {
|
|
295
|
-
const builder = query().and('_type == "course"').order('title').slice(0, 10)
|
|
296
|
-
const state = builder._state()
|
|
297
|
-
expect(state.filter).toBe('_type == "course"')
|
|
298
|
-
expect(state.ordering).toBe('| order(title)')
|
|
299
|
-
expect(state.slice).toBe('[0...10]')
|
|
300
|
-
expect(state.projection).toBe('')
|
|
301
|
-
expect(state.postFilter).toBe('')
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
test('state reflects all builder operations', () => {
|
|
305
|
-
const builder = query()
|
|
306
|
-
.and('_type == "course"')
|
|
307
|
-
.or('brand == "drumeo"', 'brand == "pianote"')
|
|
308
|
-
.select('_id', 'title')
|
|
309
|
-
.postFilter('count > 5')
|
|
310
|
-
.order('publishedOn desc')
|
|
311
|
-
.slice(0, 10)
|
|
312
|
-
const state = builder._state()
|
|
313
|
-
expect(state.filter).toBe('_type == "course" && (brand == "drumeo" || brand == "pianote")')
|
|
314
|
-
expect(state.ordering).toBe('| order(publishedOn desc)')
|
|
315
|
-
expect(state.slice).toBe('[0...10]')
|
|
316
|
-
expect(state.projection).toBe('_id, title')
|
|
317
|
-
expect(state.postFilter).toBe('count > 5')
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
test('independent builder instances have separate state', () => {
|
|
321
|
-
const builder1 = query().and('_type == "course"')
|
|
322
|
-
const builder2 = query().and('_type == "song"')
|
|
323
|
-
expect(builder1._state().filter).toBe('_type == "course"')
|
|
324
|
-
expect(builder2._state().filter).toBe('_type == "song"')
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
describe('Edge Cases', () => {
|
|
329
|
-
test('handles empty string filters', () => {
|
|
330
|
-
const result = query().and('').build()
|
|
331
|
-
expect(result).toBe('*[]')
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
test('handles empty string in or', () => {
|
|
335
|
-
const result = query().or('', 'brand == "drumeo"').build()
|
|
336
|
-
expect(result).toBe('*[brand == "drumeo"]')
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
test('handles all empty strings in or', () => {
|
|
340
|
-
const result = query().or('', '', '').build()
|
|
341
|
-
expect(result).toBe('*[]')
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
test('handles empty string in select', () => {
|
|
345
|
-
const result = query().select('', '_id', '').build()
|
|
346
|
-
expect(result).toContain('{ _id }')
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
test('handles calling first after slice', () => {
|
|
350
|
-
const result = query().slice(0, 10).first().build()
|
|
351
|
-
expect(result).toContain('[0]')
|
|
352
|
-
expect(result).not.toContain('[0...10]')
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
test('builds valid query after multiple modifications', () => {
|
|
356
|
-
const builder = query()
|
|
357
|
-
builder.and('_type == "course"')
|
|
358
|
-
builder.order('title asc')
|
|
359
|
-
builder.order('publishedOn desc') // Override
|
|
360
|
-
builder.slice(0, 10)
|
|
361
|
-
builder.slice(5, 10) // Override
|
|
362
|
-
const result = builder.build()
|
|
363
|
-
expect(result).toContain('*[_type == "course"]')
|
|
364
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
365
|
-
expect(result).toContain('[5...15]')
|
|
366
|
-
expect(result).not.toContain('title asc')
|
|
367
|
-
expect(result).not.toContain('[0...10]')
|
|
368
|
-
})
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
describe('Whitespace and Formatting', () => {
|
|
372
|
-
test('trims whitespace from built query', () => {
|
|
373
|
-
const result = query().and('_type == "course"').build()
|
|
374
|
-
expect(result).toBe('*[_type == "course"]')
|
|
375
|
-
expect(result.startsWith(' ')).toBe(false)
|
|
376
|
-
expect(result.endsWith(' ')).toBe(false)
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
test('maintains proper structure in complex queries', () => {
|
|
380
|
-
const result = query()
|
|
381
|
-
.and('_type == "course"')
|
|
382
|
-
.select('_id')
|
|
383
|
-
.order('title')
|
|
384
|
-
.slice(0, 10)
|
|
385
|
-
.build()
|
|
386
|
-
// Query should contain all expected parts
|
|
387
|
-
expect(result).toContain('*[_type == "course"]')
|
|
388
|
-
expect(result).toContain('{ _id }')
|
|
389
|
-
expect(result).toContain('| order(title)')
|
|
390
|
-
expect(result).toContain('[0...10]')
|
|
391
|
-
})
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
describe('Real-World Query Patterns', () => {
|
|
395
|
-
test('builds instructor content query', () => {
|
|
396
|
-
const result = query()
|
|
397
|
-
.and('_type == "instructor"')
|
|
398
|
-
.and('!(_id in path("drafts.**"))')
|
|
399
|
-
.select(
|
|
400
|
-
'_id',
|
|
401
|
-
'name',
|
|
402
|
-
'biography',
|
|
403
|
-
'"courseCount": count(*[_type == "course" && references(^._id)])'
|
|
404
|
-
)
|
|
405
|
-
.order('name asc')
|
|
406
|
-
.build()
|
|
407
|
-
expect(result).toContain('*[_type == "instructor"')
|
|
408
|
-
expect(result).toContain('!(_id in path("drafts.**"))')
|
|
409
|
-
expect(result).toContain('{ _id, name, biography')
|
|
410
|
-
expect(result).toContain('| order(name asc)')
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
test('builds paginated search query', () => {
|
|
414
|
-
const searchTerm = 'jazz'
|
|
415
|
-
const result = query()
|
|
416
|
-
.and('_type in ["course", "song", "play-along"]')
|
|
417
|
-
.and(`title match "*${searchTerm}*"`)
|
|
418
|
-
.and('published == true')
|
|
419
|
-
.select('_id', 'title', '_type', 'brand')
|
|
420
|
-
.order('_score desc')
|
|
421
|
-
.slice(20, 20)
|
|
422
|
-
.build()
|
|
423
|
-
expect(result).toContain('title match "*jazz*"')
|
|
424
|
-
expect(result).toContain('[20...40]')
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
test('builds content with aggregations and post-filter', () => {
|
|
428
|
-
const result = query()
|
|
429
|
-
.and('_type == "learning-path"')
|
|
430
|
-
.select(
|
|
431
|
-
'_id',
|
|
432
|
-
'title',
|
|
433
|
-
'"courses": courses[]->{ _id, title }',
|
|
434
|
-
'"totalLessons": count(courses[]->lessons[])'
|
|
435
|
-
)
|
|
436
|
-
.postFilter('totalLessons >= 10')
|
|
437
|
-
.postFilter('totalLessons <= 50')
|
|
438
|
-
.order('totalLessons desc')
|
|
439
|
-
.slice(0, 20)
|
|
440
|
-
.build()
|
|
441
|
-
expect(result).toContain('[totalLessons >= 10 && totalLessons <= 50]')
|
|
442
|
-
})
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
describe('Monoid Laws', () => {
|
|
446
|
-
test('and monoid - empty is identity (left)', () => {
|
|
447
|
-
const builder1 = query().and('').and('_type == "course"')
|
|
448
|
-
const builder2 = query().and('_type == "course"')
|
|
449
|
-
expect(builder1._state().filter).toBe(builder2._state().filter)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
test('and monoid - empty is identity (right)', () => {
|
|
453
|
-
const builder1 = query().and('_type == "course"').and('')
|
|
454
|
-
const builder2 = query().and('_type == "course"')
|
|
455
|
-
expect(builder1._state().filter).toBe(builder2._state().filter)
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
test('and monoid - associativity', () => {
|
|
459
|
-
const builder1 = query().and('a').and('b').and('c')
|
|
460
|
-
expect(builder1._state().filter).toBe('a && b && c')
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
test('or monoid - empty is identity', () => {
|
|
464
|
-
const builder1 = query().or('', 'brand == "drumeo"')
|
|
465
|
-
const builder2 = query().or('brand == "drumeo"')
|
|
466
|
-
expect(builder1._state().filter).toBe(builder2._state().filter)
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
test('or monoid - combines multiple expressions', () => {
|
|
470
|
-
const builder = query().or('a', 'b', 'c')
|
|
471
|
-
expect(builder._state().filter).toBe('((a || b) || c)')
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
test('projection monoid - accumulates fields', () => {
|
|
475
|
-
const builder = query().select('a').select('b').select('c')
|
|
476
|
-
expect(builder._state().projection).toBe('a, b, c')
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
test('projection monoid - handles empty strings', () => {
|
|
480
|
-
const builder = query().select('', 'a', '', 'b')
|
|
481
|
-
expect(builder._state().projection).toBe('a, b')
|
|
482
|
-
})
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
describe('Query Correctness', () => {
|
|
486
|
-
test('filter comes before projection', () => {
|
|
487
|
-
const result = query().and('_type == "course"').select('_id').build()
|
|
488
|
-
const filterIndex = result.indexOf('*[_type')
|
|
489
|
-
const projectionIndex = result.indexOf('{ _id }')
|
|
490
|
-
expect(filterIndex).toBeLessThan(projectionIndex)
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
test('projection comes before post-filter', () => {
|
|
494
|
-
const result = query()
|
|
495
|
-
.and('_type == "course"')
|
|
496
|
-
.select('_id', '"count": count(lessons)')
|
|
497
|
-
.postFilter('count > 5')
|
|
498
|
-
.build()
|
|
499
|
-
const projectionIndex = result.indexOf('{ _id')
|
|
500
|
-
const postFilterIndex = result.indexOf('[count > 5]')
|
|
501
|
-
expect(projectionIndex).toBeLessThan(postFilterIndex)
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
test('ordering comes after post-filter', () => {
|
|
505
|
-
const result = query()
|
|
506
|
-
.and('_type == "course"')
|
|
507
|
-
.select('_id')
|
|
508
|
-
.postFilter('count > 5')
|
|
509
|
-
.order('title')
|
|
510
|
-
.build()
|
|
511
|
-
const postFilterIndex = result.indexOf('[count > 5]')
|
|
512
|
-
const orderIndex = result.indexOf('| order')
|
|
513
|
-
expect(postFilterIndex).toBeLessThan(orderIndex)
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
test('slice comes last', () => {
|
|
517
|
-
const result = query()
|
|
518
|
-
.and('_type == "course"')
|
|
519
|
-
.select('_id')
|
|
520
|
-
.order('title')
|
|
521
|
-
.slice(0, 10)
|
|
522
|
-
.build()
|
|
523
|
-
const orderIndex = result.indexOf('| order')
|
|
524
|
-
const sliceIndex = result.indexOf('[0...10]')
|
|
525
|
-
expect(orderIndex).toBeLessThan(sliceIndex)
|
|
526
|
-
})
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
describe('Query Selector', () => {
|
|
530
|
-
describe('Default Selector Behavior', () => {
|
|
531
|
-
test('defaults to * selector when no argument provided', () => {
|
|
532
|
-
const result = query().build()
|
|
533
|
-
expect(result).toBe('*[]')
|
|
534
|
-
expect(result.startsWith('*[')).toBe(true)
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
test('uses default * selector with filters', () => {
|
|
538
|
-
const result = query().and('_type == "course"').build()
|
|
539
|
-
expect(result).toBe('*[_type == "course"]')
|
|
540
|
-
expect(result.startsWith('*[')).toBe(true)
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
test('state shows default * selector', () => {
|
|
544
|
-
const builder = query()
|
|
545
|
-
expect(builder._state().selector).toBe('*')
|
|
546
|
-
})
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
describe('Setting Selector via Constructor', () => {
|
|
550
|
-
test('accepts custom selector in constructor', () => {
|
|
551
|
-
const result = query('*[_type == "course"]').build()
|
|
552
|
-
expect(result).toBe('*[_type == "course"][]')
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
test('uses constructor selector with additional filters', () => {
|
|
556
|
-
const result = query('*[_type == "course"]').and('published == true').build()
|
|
557
|
-
expect(result).toBe('*[_type == "course"][published == true]')
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
test('uses constructor selector with projection', () => {
|
|
561
|
-
const result = query('*[_type == "course"]').select('_id', 'title').build()
|
|
562
|
-
expect(result).toContain('*[_type == "course"][]')
|
|
563
|
-
expect(result).toContain('{ _id, title }')
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
test('uses constructor selector with ordering and slicing', () => {
|
|
567
|
-
const result = query('*[_type == "course"]').order('publishedOn desc').slice(0, 10).build()
|
|
568
|
-
expect(result).toContain('*[_type == "course"][]')
|
|
569
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
570
|
-
expect(result).toContain('[0...10]')
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
test('constructor selector with references', () => {
|
|
574
|
-
const result = query('*[_type == "learning-path"]->courses[]').build()
|
|
575
|
-
expect(result).toBe('*[_type == "learning-path"]->courses[][]')
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
test('constructor selector with nested query', () => {
|
|
579
|
-
const selector =
|
|
580
|
-
'*[_type == "instructor" && count(*[_type == "course" && references(^._id)]) > 5]'
|
|
581
|
-
const result = query(selector).build()
|
|
582
|
-
expect(result).toContain(selector)
|
|
583
|
-
expect(result).toBe(`${selector}[]`)
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
test('state reflects constructor selector', () => {
|
|
587
|
-
const builder = query('*[_type == "course"]')
|
|
588
|
-
expect(builder._state().selector).toBe('*[_type == "course"]')
|
|
589
|
-
})
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
describe('Setting Selector via Method', () => {
|
|
593
|
-
test('changes selector via method call', () => {
|
|
594
|
-
const result = query().selector('*[_type == "song"]').build()
|
|
595
|
-
expect(result).toBe('*[_type == "song"][]')
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
test('selector method returns builder for chaining', () => {
|
|
599
|
-
const builder = query()
|
|
600
|
-
const result = builder.selector('*[_type == "course"]')
|
|
601
|
-
expect(result).toBe(builder)
|
|
602
|
-
})
|
|
603
|
-
|
|
604
|
-
test('selector method allows full chaining', () => {
|
|
605
|
-
const result = query()
|
|
606
|
-
.selector('*[_type == "course"]')
|
|
607
|
-
.and('published == true')
|
|
608
|
-
.select('_id', 'title')
|
|
609
|
-
.order('publishedOn desc')
|
|
610
|
-
.slice(0, 10)
|
|
611
|
-
.build()
|
|
612
|
-
expect(result).toContain('*[_type == "course"][published == true]')
|
|
613
|
-
expect(result).toContain('{ _id, title }')
|
|
614
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
615
|
-
expect(result).toContain('[0...10]')
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
test('selector method overrides default selector', () => {
|
|
619
|
-
const builder = query()
|
|
620
|
-
expect(builder._state().selector).toBe('*')
|
|
621
|
-
builder.selector('*[_type == "course"]')
|
|
622
|
-
expect(builder._state().selector).toBe('*[_type == "course"]')
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
test('selector method can be called before other operations', () => {
|
|
626
|
-
const result = query().selector('*[_type == "course"]').and('brand == "drumeo"').build()
|
|
627
|
-
expect(result).toBe('*[_type == "course"][brand == "drumeo"]')
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
test('selector method can be called after other operations', () => {
|
|
631
|
-
const result = query().and('brand == "drumeo"').selector('*[_type == "course"]').build()
|
|
632
|
-
expect(result).toBe('*[_type == "course"][brand == "drumeo"]')
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
test('state reflects method selector', () => {
|
|
636
|
-
const builder = query().selector('*[_type == "song"]')
|
|
637
|
-
expect(builder._state().selector).toBe('*[_type == "song"]')
|
|
638
|
-
})
|
|
639
|
-
})
|
|
640
|
-
|
|
641
|
-
describe('Selector Override Behavior', () => {
|
|
642
|
-
test('method selector overrides constructor selector', () => {
|
|
643
|
-
const result = query('*[_type == "course"]').selector('*[_type == "song"]').build()
|
|
644
|
-
expect(result).toBe('*[_type == "song"][]')
|
|
645
|
-
expect(result).not.toContain('course')
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
test('last selector call wins with multiple method calls', () => {
|
|
649
|
-
const builder = query()
|
|
650
|
-
.selector('*[_type == "course"]')
|
|
651
|
-
.selector('*[_type == "song"]')
|
|
652
|
-
.selector('*[_type == "play-along"]')
|
|
653
|
-
expect(builder._state().selector).toBe('*[_type == "play-along"]')
|
|
654
|
-
expect(builder.build()).toBe('*[_type == "play-along"][]')
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
test('can override back to default selector', () => {
|
|
658
|
-
const result = query('*[_type == "course"]').selector('*').and('published == true').build()
|
|
659
|
-
expect(result).toBe('*[published == true]')
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
test('state updates correctly with overrides', () => {
|
|
663
|
-
const builder = query('*[_type == "course"]')
|
|
664
|
-
expect(builder._state().selector).toBe('*[_type == "course"]')
|
|
665
|
-
builder.selector('*[_type == "song"]')
|
|
666
|
-
expect(builder._state().selector).toBe('*[_type == "song"]')
|
|
667
|
-
builder.selector('*')
|
|
668
|
-
expect(builder._state().selector).toBe('*')
|
|
669
|
-
})
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
describe('Selector with Complex Operations', () => {
|
|
673
|
-
test('selector with all query features', () => {
|
|
674
|
-
const result = query('*[_type == "learning-path"]->courses[]')
|
|
675
|
-
.and('published == true')
|
|
676
|
-
.or('difficulty == "beginner"', 'difficulty == "intermediate"')
|
|
677
|
-
.select('_id', 'title', '"lessonCount": count(lessons)')
|
|
678
|
-
.postFilter('lessonCount > 5')
|
|
679
|
-
.order('publishedOn desc')
|
|
680
|
-
.slice(0, 20)
|
|
681
|
-
.build()
|
|
682
|
-
expect(result).toContain('*[_type == "learning-path"]->courses[]')
|
|
683
|
-
expect(result).toContain(
|
|
684
|
-
'[published == true && (difficulty == "beginner" || difficulty == "intermediate")]'
|
|
685
|
-
)
|
|
686
|
-
expect(result).toContain('{ _id, title, "lessonCount": count(lessons) }')
|
|
687
|
-
expect(result).toContain('[lessonCount > 5]')
|
|
688
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
689
|
-
expect(result).toContain('[0...20]')
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
test('selector with references and joins', () => {
|
|
693
|
-
const selector = '*[_type == "course"]->instructor'
|
|
694
|
-
const result = query(selector)
|
|
695
|
-
.and('active == true')
|
|
696
|
-
.select('_id', 'name', '"totalCourses": count(*[_type == "course" && references(^._id)])')
|
|
697
|
-
.build()
|
|
698
|
-
expect(result).toContain(selector)
|
|
699
|
-
expect(result).toContain('[active == true]')
|
|
700
|
-
expect(result).toContain('{ _id, name, "totalCourses": count')
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
test('selector with array dereferencing', () => {
|
|
704
|
-
const selector = '*[_type == "playlist"]->items[]->content'
|
|
705
|
-
const result = query(selector)
|
|
706
|
-
.and('_type in ["course", "song"]')
|
|
707
|
-
.select('_id', 'title', '_type')
|
|
708
|
-
.build()
|
|
709
|
-
expect(result).toContain(selector)
|
|
710
|
-
expect(result).toContain('[_type in ["course", "song"]]')
|
|
711
|
-
})
|
|
712
|
-
|
|
713
|
-
test('complex selector with conditional logic', () => {
|
|
714
|
-
const selector = '*[_type == "user" && _id == $userId]'
|
|
715
|
-
const result = query(selector)
|
|
716
|
-
.select('"enrollments": *[_type == "enrollment" && user._ref == ^._id]')
|
|
717
|
-
.build()
|
|
718
|
-
expect(result).toContain(selector)
|
|
719
|
-
expect(result).toContain('{ "enrollments":')
|
|
720
|
-
})
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
describe('Selector State Independence', () => {
|
|
724
|
-
test('independent builder instances have independent selectors', () => {
|
|
725
|
-
const builder1 = query('*[_type == "course"]')
|
|
726
|
-
const builder2 = query('*[_type == "song"]')
|
|
727
|
-
expect(builder1._state().selector).toBe('*[_type == "course"]')
|
|
728
|
-
expect(builder2._state().selector).toBe('*[_type == "song"]')
|
|
729
|
-
})
|
|
730
|
-
|
|
731
|
-
test('modifying one builder does not affect another', () => {
|
|
732
|
-
const builder1 = query()
|
|
733
|
-
const builder2 = query()
|
|
734
|
-
builder1.selector('*[_type == "course"]')
|
|
735
|
-
expect(builder1._state().selector).toBe('*[_type == "course"]')
|
|
736
|
-
expect(builder2._state().selector).toBe('*')
|
|
737
|
-
})
|
|
738
|
-
|
|
739
|
-
test('state references the same object', () => {
|
|
740
|
-
const builder = query('*[_type == "course"]')
|
|
741
|
-
const state1 = builder._state()
|
|
742
|
-
builder.selector('*[_type == "song"]')
|
|
743
|
-
const state2 = builder._state()
|
|
744
|
-
expect(state1).toBe(state2)
|
|
745
|
-
expect(state1.selector).toBe('*[_type == "song"]')
|
|
746
|
-
expect(state2.selector).toBe('*[_type == "song"]')
|
|
747
|
-
})
|
|
748
|
-
})
|
|
749
|
-
|
|
750
|
-
describe('Selector Edge Cases', () => {
|
|
751
|
-
test('empty string selector in constructor defaults to *', () => {
|
|
752
|
-
const result = query('').build()
|
|
753
|
-
expect(result).toBe('*[]')
|
|
754
|
-
const builder = query('')
|
|
755
|
-
expect(builder._state().selector).toBe('*')
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
test('empty string selector via method sets empty selector', () => {
|
|
759
|
-
const result = query().selector('').and('_type == "course"').build()
|
|
760
|
-
expect(result).toBe('[_type == "course"]')
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
test('handles selector with special characters', () => {
|
|
764
|
-
const selector = '*[title match "foo*bar" && _type == "course"]'
|
|
765
|
-
const result = query(selector).build()
|
|
766
|
-
expect(result).toContain(selector)
|
|
767
|
-
})
|
|
768
|
-
|
|
769
|
-
test('handles selector with parameterized queries', () => {
|
|
770
|
-
const selector = '*[_type == $contentType && brand == $brand]'
|
|
771
|
-
const result = query(selector).and('published == true').build()
|
|
772
|
-
expect(result).toContain(selector)
|
|
773
|
-
expect(result).toContain('[published == true]')
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
test('handles very long selector', () => {
|
|
777
|
-
const selector =
|
|
778
|
-
'*[_type == "course" && published == true && !(_id in path("drafts.**")) && brand in ["drumeo", "pianote"]]'
|
|
779
|
-
const result = query(selector).select('_id').build()
|
|
780
|
-
expect(result).toContain(selector)
|
|
781
|
-
expect(result).toContain('{ _id }')
|
|
782
|
-
})
|
|
783
|
-
|
|
784
|
-
test('handles selector with nested brackets', () => {
|
|
785
|
-
const selector = '*[_type == "course" && count(lessons[published == true]) > 5]'
|
|
786
|
-
const result = query(selector).build()
|
|
787
|
-
expect(result).toContain(selector)
|
|
788
|
-
})
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
describe('Real-World Selector Use Cases', () => {
|
|
792
|
-
test('querying referenced instructor from courses', () => {
|
|
793
|
-
const result = query('*[_type == "course"]->instructor')
|
|
794
|
-
.and('active == true')
|
|
795
|
-
.select('_id', 'name', 'biography')
|
|
796
|
-
.order('name asc')
|
|
797
|
-
.build()
|
|
798
|
-
expect(result).toContain('*[_type == "course"]->instructor')
|
|
799
|
-
expect(result).toContain('[active == true]')
|
|
800
|
-
expect(result).toContain('{ _id, name, biography }')
|
|
801
|
-
})
|
|
802
|
-
|
|
803
|
-
test('querying learning path courses', () => {
|
|
804
|
-
const result = query(
|
|
805
|
-
'*[_type == "learning-path-v2" && slug.current == $slug][0]->courses[]'
|
|
806
|
-
)
|
|
807
|
-
.and('published == true')
|
|
808
|
-
.select('_id', 'title', 'difficulty')
|
|
809
|
-
.build()
|
|
810
|
-
expect(result).toContain(
|
|
811
|
-
'*[_type == "learning-path-v2" && slug.current == $slug][0]->courses[]'
|
|
812
|
-
)
|
|
813
|
-
})
|
|
814
|
-
|
|
815
|
-
test('querying user enrollments', () => {
|
|
816
|
-
const result = query('*[_type == "enrollment" && user._ref == $userId]')
|
|
817
|
-
.select(
|
|
818
|
-
'_id',
|
|
819
|
-
'enrolledAt',
|
|
820
|
-
'"content": content->{ _id, title, _type }',
|
|
821
|
-
'"progress": *[_type == "progress" && user._ref == $userId && content._ref == ^.content._ref][0]'
|
|
822
|
-
)
|
|
823
|
-
.order('enrolledAt desc')
|
|
824
|
-
.slice(0, 50)
|
|
825
|
-
.build()
|
|
826
|
-
expect(result).toContain('*[_type == "enrollment" && user._ref == $userId]')
|
|
827
|
-
expect(result).toContain('"progress":')
|
|
828
|
-
})
|
|
829
|
-
|
|
830
|
-
test('querying content with instructor filter', () => {
|
|
831
|
-
const result = query('*[_type in ["course", "song"] && instructor._ref == $instructorId]')
|
|
832
|
-
.and('published == true')
|
|
833
|
-
.select('_id', 'title', '_type', 'publishedOn')
|
|
834
|
-
.order('publishedOn desc')
|
|
835
|
-
.build()
|
|
836
|
-
expect(result).toContain(
|
|
837
|
-
'*[_type in ["course", "song"] && instructor._ref == $instructorId]'
|
|
838
|
-
)
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
test('querying pack contents', () => {
|
|
842
|
-
const result = query('*[_type == "pack" && _id == $packId][0]->contents[]')
|
|
843
|
-
.select('_id', 'title', '_type', '"thumbnail": thumbnail.asset->url')
|
|
844
|
-
.build()
|
|
845
|
-
expect(result).toContain('*[_type == "pack" && _id == $packId][0]->contents[]')
|
|
846
|
-
})
|
|
847
|
-
|
|
848
|
-
test('querying related content', () => {
|
|
849
|
-
const result = query('*[_type == $contentType && references($contentId)]')
|
|
850
|
-
.and('!(_id in path("drafts.**"))')
|
|
851
|
-
.select('_id', 'title', '_type')
|
|
852
|
-
.slice(0, 10)
|
|
853
|
-
.build()
|
|
854
|
-
expect(result).toContain('*[_type == $contentType && references($contentId)]')
|
|
855
|
-
expect(result).toContain('!(_id in path("drafts.**"))')
|
|
856
|
-
})
|
|
857
|
-
})
|
|
858
|
-
|
|
859
|
-
describe('Selector Query Structure Validation', () => {
|
|
860
|
-
test('selector appears at start of built query', () => {
|
|
861
|
-
const selector = '*[_type == "course"]'
|
|
862
|
-
const result = query(selector).and('published == true').build()
|
|
863
|
-
expect(result.indexOf(selector)).toBe(0)
|
|
864
|
-
})
|
|
865
|
-
|
|
866
|
-
test('selector followed by filter brackets', () => {
|
|
867
|
-
const result = query('*[_type == "course"]').and('published == true').build()
|
|
868
|
-
expect(result).toMatch(/^\*\[_type == "course"\]\[published == true\]/)
|
|
869
|
-
})
|
|
870
|
-
|
|
871
|
-
test('custom selector maintains proper query order', () => {
|
|
872
|
-
const selector = '*[_type == "learning-path"]->courses[]'
|
|
873
|
-
const result = query(selector)
|
|
874
|
-
.and('published == true')
|
|
875
|
-
.select('_id')
|
|
876
|
-
.postFilter('count > 5')
|
|
877
|
-
.order('title')
|
|
878
|
-
.slice(0, 10)
|
|
879
|
-
.build()
|
|
880
|
-
|
|
881
|
-
const selectorIndex = result.indexOf(selector)
|
|
882
|
-
const filterIndex = result.indexOf('[published == true]')
|
|
883
|
-
const projectionIndex = result.indexOf('{ _id }')
|
|
884
|
-
const postFilterIndex = result.indexOf('[count > 5]')
|
|
885
|
-
const orderIndex = result.indexOf('| order')
|
|
886
|
-
const sliceIndex = result.indexOf('[0...10]')
|
|
887
|
-
|
|
888
|
-
expect(selectorIndex).toBe(0)
|
|
889
|
-
expect(selectorIndex).toBeLessThan(filterIndex)
|
|
890
|
-
expect(filterIndex).toBeLessThan(projectionIndex)
|
|
891
|
-
expect(projectionIndex).toBeLessThan(postFilterIndex)
|
|
892
|
-
expect(postFilterIndex).toBeLessThan(orderIndex)
|
|
893
|
-
expect(orderIndex).toBeLessThan(sliceIndex)
|
|
894
|
-
})
|
|
895
|
-
})
|
|
896
|
-
})
|
|
897
|
-
|
|
898
|
-
describe('Query Accessor', () => {
|
|
899
|
-
describe('Basic Accessor Functionality', () => {
|
|
900
|
-
test('default accessor is empty string', () => {
|
|
901
|
-
const builder = query()
|
|
902
|
-
expect(builder._state().accessor).toBe('')
|
|
903
|
-
})
|
|
904
|
-
|
|
905
|
-
test('builds query with simple field access', () => {
|
|
906
|
-
const result = query().access('title').build()
|
|
907
|
-
expect(result).toBe('*[].title')
|
|
908
|
-
})
|
|
909
|
-
|
|
910
|
-
test('builds query with field access using dereference', () => {
|
|
911
|
-
const result = query().access('instructor').dereference().build()
|
|
912
|
-
expect(result).toBe('*[].instructor->')
|
|
913
|
-
})
|
|
914
|
-
|
|
915
|
-
test('builds query with array notation access', () => {
|
|
916
|
-
const result = query().access('items[]').build()
|
|
917
|
-
expect(result).toBe('*[].items[]')
|
|
918
|
-
})
|
|
919
|
-
|
|
920
|
-
test('access method returns builder for chaining', () => {
|
|
921
|
-
const builder = query()
|
|
922
|
-
const result = builder.access('field')
|
|
923
|
-
expect(result).toBe(builder)
|
|
924
|
-
})
|
|
925
|
-
|
|
926
|
-
test('state reflects accessor value', () => {
|
|
927
|
-
const builder = query().access('title')
|
|
928
|
-
expect(builder._state().accessor).toBe('.title')
|
|
929
|
-
})
|
|
930
|
-
})
|
|
931
|
-
|
|
932
|
-
describe('Accessor with Filters', () => {
|
|
933
|
-
test('builds query with accessor after filter brackets', () => {
|
|
934
|
-
const result = query().and('_type == "course"').access('title').build()
|
|
935
|
-
expect(result).toBe('*[_type == "course"].title')
|
|
936
|
-
})
|
|
937
|
-
|
|
938
|
-
test('builds query with accessor and single and filter', () => {
|
|
939
|
-
const result = query().and('_type == "course"').access('instructor').dereference().build()
|
|
940
|
-
expect(result).toBe('*[_type == "course"].instructor->')
|
|
941
|
-
})
|
|
942
|
-
|
|
943
|
-
test('builds query with accessor and multiple and filters', () => {
|
|
944
|
-
const result = query()
|
|
945
|
-
.and('_type == "course"')
|
|
946
|
-
.and('published == true')
|
|
947
|
-
.access('title')
|
|
948
|
-
.build()
|
|
949
|
-
expect(result).toBe('*[_type == "course" && published == true].title')
|
|
950
|
-
})
|
|
951
|
-
|
|
952
|
-
test('builds query with accessor and or filters', () => {
|
|
953
|
-
const result = query().or('brand == "drumeo"', 'brand == "pianote"').access('title').build()
|
|
954
|
-
expect(result).toBe('*[(brand == "drumeo" || brand == "pianote")].title')
|
|
955
|
-
})
|
|
956
|
-
|
|
957
|
-
test('builds query with accessor and complex filter combinations', () => {
|
|
958
|
-
const result = query()
|
|
959
|
-
.and('_type == "course"')
|
|
960
|
-
.and('published == true')
|
|
961
|
-
.or('difficulty == "beginner"', 'difficulty == "intermediate"')
|
|
962
|
-
.access('lessons[]')
|
|
963
|
-
.dereference()
|
|
964
|
-
.build()
|
|
965
|
-
expect(result).toContain('*[_type == "course" && published == true')
|
|
966
|
-
expect(result).toContain('(difficulty == "beginner" || difficulty == "intermediate")]')
|
|
967
|
-
expect(result).toContain('.lessons[]->')
|
|
968
|
-
})
|
|
969
|
-
})
|
|
970
|
-
|
|
971
|
-
describe('Accessor with Projection', () => {
|
|
972
|
-
test('builds query with accessor before projection', () => {
|
|
973
|
-
const result = query().access('instructor').dereference().select('_id', 'name').build()
|
|
974
|
-
expect(result).toContain('*[].instructor->')
|
|
975
|
-
expect(result).toContain('{ _id, name }')
|
|
976
|
-
const accessorIndex = result.indexOf('.instructor->')
|
|
977
|
-
const projectionIndex = result.indexOf('{ _id, name }')
|
|
978
|
-
expect(accessorIndex).toBeLessThan(projectionIndex)
|
|
979
|
-
})
|
|
980
|
-
|
|
981
|
-
test('builds query with accessor and single field selection', () => {
|
|
982
|
-
const result = query()
|
|
983
|
-
.and('_type == "course"')
|
|
984
|
-
.access('instructor')
|
|
985
|
-
.dereference()
|
|
986
|
-
.select('_id')
|
|
987
|
-
.build()
|
|
988
|
-
expect(result).toContain('*[_type == "course"].instructor->')
|
|
989
|
-
expect(result).toContain('{ _id }')
|
|
990
|
-
})
|
|
991
|
-
|
|
992
|
-
test('builds query with accessor and multiple field selections', () => {
|
|
993
|
-
const result = query()
|
|
994
|
-
.and('_type == "course"')
|
|
995
|
-
.access('instructor')
|
|
996
|
-
.dereference()
|
|
997
|
-
.select('_id', 'name', 'biography')
|
|
998
|
-
.build()
|
|
999
|
-
expect(result).toContain('*[_type == "course"].instructor->')
|
|
1000
|
-
expect(result).toContain('{ _id, name, biography }')
|
|
1001
|
-
})
|
|
1002
|
-
|
|
1003
|
-
test('builds query with accessor and complex projection', () => {
|
|
1004
|
-
const result = query()
|
|
1005
|
-
.and('_type == "learning-path"')
|
|
1006
|
-
.access('courses[]')
|
|
1007
|
-
.dereference()
|
|
1008
|
-
.select('_id', 'title', '"instructor": instructor->name')
|
|
1009
|
-
.build()
|
|
1010
|
-
expect(result).toContain('*[_type == "learning-path"].courses[]->')
|
|
1011
|
-
expect(result).toContain('{ _id, title, "instructor": instructor->name }')
|
|
1012
|
-
})
|
|
1013
|
-
})
|
|
1014
|
-
|
|
1015
|
-
describe('Accessor with Other Operations', () => {
|
|
1016
|
-
test('builds query with accessor and ordering', () => {
|
|
1017
|
-
const result = query()
|
|
1018
|
-
.and('_type == "course"')
|
|
1019
|
-
.access('lessons[]')
|
|
1020
|
-
.order('publishedOn desc')
|
|
1021
|
-
.build()
|
|
1022
|
-
expect(result).toContain('*[_type == "course"].lessons[]')
|
|
1023
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
1024
|
-
})
|
|
1025
|
-
|
|
1026
|
-
test('builds query with accessor and slicing', () => {
|
|
1027
|
-
const result = query().and('_type == "course"').access('lessons[]').slice(0, 10).build()
|
|
1028
|
-
expect(result).toContain('*[_type == "course"].lessons[]')
|
|
1029
|
-
expect(result).toContain('[0...10]')
|
|
1030
|
-
})
|
|
1031
|
-
|
|
1032
|
-
test('builds query with accessor and post-filters', () => {
|
|
1033
|
-
const result = query()
|
|
1034
|
-
.and('_type == "course"')
|
|
1035
|
-
.access('lessons[]')
|
|
1036
|
-
.dereference()
|
|
1037
|
-
.select('_id', '"count": count(videos)')
|
|
1038
|
-
.postFilter('count > 5')
|
|
1039
|
-
.build()
|
|
1040
|
-
expect(result).toContain('*[_type == "course"].lessons[]->')
|
|
1041
|
-
expect(result).toContain('{ _id, "count": count(videos) }')
|
|
1042
|
-
expect(result).toContain('[count > 5]')
|
|
1043
|
-
})
|
|
1044
|
-
|
|
1045
|
-
test('builds query with accessor and first() helper', () => {
|
|
1046
|
-
const result = query()
|
|
1047
|
-
.and('_type == "course"')
|
|
1048
|
-
.access('instructor')
|
|
1049
|
-
.dereference()
|
|
1050
|
-
.first()
|
|
1051
|
-
.build()
|
|
1052
|
-
expect(result).toContain('*[_type == "course"].instructor->')
|
|
1053
|
-
expect(result).toContain('[0]')
|
|
1054
|
-
})
|
|
1055
|
-
|
|
1056
|
-
test('builds query with accessor and all query features combined', () => {
|
|
1057
|
-
const result = query()
|
|
1058
|
-
.and('_type == "course"')
|
|
1059
|
-
.and('published == true')
|
|
1060
|
-
.or('difficulty == "beginner"', 'difficulty == "intermediate"')
|
|
1061
|
-
.access('lessons[]')
|
|
1062
|
-
.dereference()
|
|
1063
|
-
.select('_id', 'title', '"videoCount": count(videos)')
|
|
1064
|
-
.postFilter('videoCount > 3')
|
|
1065
|
-
.order('publishedOn desc')
|
|
1066
|
-
.slice(0, 20)
|
|
1067
|
-
.build()
|
|
1068
|
-
expect(result).toContain('*[_type == "course" && published == true')
|
|
1069
|
-
expect(result).toContain('(difficulty == "beginner" || difficulty == "intermediate")]')
|
|
1070
|
-
expect(result).toContain('.lessons[]->')
|
|
1071
|
-
expect(result).toContain('{ _id, title, "videoCount": count(videos) }')
|
|
1072
|
-
expect(result).toContain('[videoCount > 3]')
|
|
1073
|
-
expect(result).toContain('| order(publishedOn desc)')
|
|
1074
|
-
expect(result).toContain('[0...20]')
|
|
1075
|
-
})
|
|
1076
|
-
|
|
1077
|
-
test('accessor maintains position with multiple operations', () => {
|
|
1078
|
-
const result = query()
|
|
1079
|
-
.and('_type == "course"')
|
|
1080
|
-
.access('instructor')
|
|
1081
|
-
.dereference()
|
|
1082
|
-
.select('_id')
|
|
1083
|
-
.order('name')
|
|
1084
|
-
.slice(0, 5)
|
|
1085
|
-
.build()
|
|
1086
|
-
const filterIndex = result.indexOf('[_type == "course"]')
|
|
1087
|
-
const accessorIndex = result.indexOf('.instructor->')
|
|
1088
|
-
const projectionIndex = result.indexOf('{ _id }')
|
|
1089
|
-
const orderIndex = result.indexOf('| order')
|
|
1090
|
-
const sliceIndex = result.indexOf('[0...5]')
|
|
1091
|
-
expect(filterIndex).toBeLessThan(accessorIndex)
|
|
1092
|
-
expect(accessorIndex).toBeLessThan(projectionIndex)
|
|
1093
|
-
expect(projectionIndex).toBeLessThan(orderIndex)
|
|
1094
|
-
expect(orderIndex).toBeLessThan(sliceIndex)
|
|
1095
|
-
})
|
|
1096
|
-
})
|
|
1097
|
-
|
|
1098
|
-
describe('Accessor Override Behavior', () => {
|
|
1099
|
-
test('last access call overrides previous', () => {
|
|
1100
|
-
const result = query()
|
|
1101
|
-
.and('_type == "course"')
|
|
1102
|
-
.access('oldField')
|
|
1103
|
-
.access('newField')
|
|
1104
|
-
.build()
|
|
1105
|
-
expect(result).toBe('*[_type == "course"].newField')
|
|
1106
|
-
expect(result).not.toContain('oldField')
|
|
1107
|
-
})
|
|
1108
|
-
|
|
1109
|
-
test('overrides field name', () => {
|
|
1110
|
-
const builder = query().access('oldField')
|
|
1111
|
-
expect(builder._state().accessor).toBe('.oldField')
|
|
1112
|
-
builder.access('newField')
|
|
1113
|
-
expect(builder._state().accessor).toBe('.newField')
|
|
1114
|
-
})
|
|
1115
|
-
|
|
1116
|
-
test('empty string clears accessor', () => {
|
|
1117
|
-
const result = query().and('_type == "course"').access('title').access('').build()
|
|
1118
|
-
expect(result).toBe('*[_type == "course"]')
|
|
1119
|
-
expect(result).not.toContain('.title')
|
|
1120
|
-
})
|
|
1121
|
-
|
|
1122
|
-
test('overrides plain accessor with dereferenced accessor', () => {
|
|
1123
|
-
const result = query()
|
|
1124
|
-
.and('_type == "course"')
|
|
1125
|
-
.access('field')
|
|
1126
|
-
.access('other')
|
|
1127
|
-
.dereference()
|
|
1128
|
-
.build()
|
|
1129
|
-
expect(result).toBe('*[_type == "course"].other->')
|
|
1130
|
-
expect(result).not.toContain('.field')
|
|
1131
|
-
})
|
|
1132
|
-
})
|
|
1133
|
-
|
|
1134
|
-
describe('Accessor with Custom Selectors', () => {
|
|
1135
|
-
test('builds query with accessor and custom selector from constructor', () => {
|
|
1136
|
-
const result = query('*[_type == "course"]').access('title').build()
|
|
1137
|
-
expect(result).toBe('*[_type == "course"][].title')
|
|
1138
|
-
})
|
|
1139
|
-
|
|
1140
|
-
test('builds query with accessor and selector() method', () => {
|
|
1141
|
-
const result = query()
|
|
1142
|
-
.selector('*[_type == "learning-path"]')
|
|
1143
|
-
.access('courses[]')
|
|
1144
|
-
.dereference()
|
|
1145
|
-
.build()
|
|
1146
|
-
expect(result).toBe('*[_type == "learning-path"][].courses[]->')
|
|
1147
|
-
})
|
|
1148
|
-
|
|
1149
|
-
test('builds query with accessor and selector containing dereference', () => {
|
|
1150
|
-
const result = query('*[_type == "course"]->instructor')
|
|
1151
|
-
.and('active == true')
|
|
1152
|
-
.access('courses[]')
|
|
1153
|
-
.select('_id', 'title')
|
|
1154
|
-
.build()
|
|
1155
|
-
expect(result).toContain('*[_type == "course"]->instructor[active == true].courses[]')
|
|
1156
|
-
expect(result).toContain('{ _id, title }')
|
|
1157
|
-
})
|
|
1158
|
-
|
|
1159
|
-
test('builds query with accessor and complex custom selector', () => {
|
|
1160
|
-
const selector = '*[_type == "user" && _id == $userId][0]'
|
|
1161
|
-
const result = query(selector)
|
|
1162
|
-
.access('enrollments[]')
|
|
1163
|
-
.dereference()
|
|
1164
|
-
.select('_id', 'enrolledAt', '"content": content->title')
|
|
1165
|
-
.order('enrolledAt desc')
|
|
1166
|
-
.build()
|
|
1167
|
-
expect(result).toContain(selector)
|
|
1168
|
-
expect(result).toContain('.enrollments[]->')
|
|
1169
|
-
expect(result).toContain('{ _id, enrolledAt, "content": content->title }')
|
|
1170
|
-
})
|
|
1171
|
-
})
|
|
1172
|
-
|
|
1173
|
-
describe('Accessor State Management', () => {
|
|
1174
|
-
test('_state() exposes accessor value', () => {
|
|
1175
|
-
const builder = query().access('title')
|
|
1176
|
-
const state = builder._state()
|
|
1177
|
-
expect(state.accessor).toBe('.title')
|
|
1178
|
-
})
|
|
1179
|
-
|
|
1180
|
-
test('independent builder instances have independent accessors', () => {
|
|
1181
|
-
const builder1 = query().access('field1')
|
|
1182
|
-
const builder2 = query().access('field2')
|
|
1183
|
-
expect(builder1._state().accessor).toBe('.field1')
|
|
1184
|
-
expect(builder2._state().accessor).toBe('.field2')
|
|
1185
|
-
})
|
|
1186
|
-
|
|
1187
|
-
test('state updates when accessor changes', () => {
|
|
1188
|
-
const builder = query()
|
|
1189
|
-
expect(builder._state().accessor).toBe('')
|
|
1190
|
-
builder.access('first')
|
|
1191
|
-
expect(builder._state().accessor).toBe('.first')
|
|
1192
|
-
builder.access('second').dereference()
|
|
1193
|
-
expect(builder._state().accessor).toBe('.second')
|
|
1194
|
-
expect(builder._state().dereference).toBe('->')
|
|
1195
|
-
builder.access('')
|
|
1196
|
-
expect(builder._state().accessor).toBe('')
|
|
1197
|
-
})
|
|
1198
|
-
|
|
1199
|
-
test('accessor state is empty by default', () => {
|
|
1200
|
-
const builder = query()
|
|
1201
|
-
expect(builder._state().accessor).toBe('')
|
|
1202
|
-
})
|
|
1203
|
-
})
|
|
1204
|
-
|
|
1205
|
-
describe('Accessor Position Validation', () => {
|
|
1206
|
-
test('accessor appears immediately after filter brackets', () => {
|
|
1207
|
-
const result = query().and('_type == "course"').access('title').build()
|
|
1208
|
-
const filterEndIndex = result.indexOf(']')
|
|
1209
|
-
const accessorIndex = result.indexOf('.title')
|
|
1210
|
-
expect(accessorIndex).toBe(filterEndIndex + 1)
|
|
1211
|
-
})
|
|
1212
|
-
|
|
1213
|
-
test('accessor appears before projection', () => {
|
|
1214
|
-
const result = query()
|
|
1215
|
-
.and('_type == "course"')
|
|
1216
|
-
.access('instructor')
|
|
1217
|
-
.dereference()
|
|
1218
|
-
.select('_id', 'name')
|
|
1219
|
-
.build()
|
|
1220
|
-
const accessorIndex = result.indexOf('.instructor->')
|
|
1221
|
-
const projectionIndex = result.indexOf('{ _id, name }')
|
|
1222
|
-
expect(accessorIndex).toBeLessThan(projectionIndex)
|
|
1223
|
-
})
|
|
1224
|
-
|
|
1225
|
-
test('accessor maintains correct position in complex queries', () => {
|
|
1226
|
-
const result = query()
|
|
1227
|
-
.and('_type == "course"')
|
|
1228
|
-
.access('lessons[]')
|
|
1229
|
-
.dereference()
|
|
1230
|
-
.select('_id')
|
|
1231
|
-
.postFilter('count > 5')
|
|
1232
|
-
.order('publishedOn desc')
|
|
1233
|
-
.slice(0, 10)
|
|
1234
|
-
.build()
|
|
1235
|
-
const filterIndex = result.indexOf('[_type == "course"]')
|
|
1236
|
-
const accessorIndex = result.indexOf('.lessons[]->')
|
|
1237
|
-
const projectionIndex = result.indexOf('{ _id }')
|
|
1238
|
-
const postFilterIndex = result.indexOf('[count > 5]')
|
|
1239
|
-
const orderIndex = result.indexOf('| order')
|
|
1240
|
-
const sliceIndex = result.indexOf('[0...10]')
|
|
1241
|
-
|
|
1242
|
-
expect(filterIndex).toBeLessThan(accessorIndex)
|
|
1243
|
-
expect(accessorIndex).toBeLessThan(projectionIndex)
|
|
1244
|
-
expect(projectionIndex).toBeLessThan(postFilterIndex)
|
|
1245
|
-
expect(postFilterIndex).toBeLessThan(orderIndex)
|
|
1246
|
-
expect(orderIndex).toBeLessThan(sliceIndex)
|
|
1247
|
-
})
|
|
1248
|
-
|
|
1249
|
-
test('validates complete query order with accessor', () => {
|
|
1250
|
-
const result = query()
|
|
1251
|
-
.selector('*[_type == "learning-path"]')
|
|
1252
|
-
.and('published == true')
|
|
1253
|
-
.access('courses[]')
|
|
1254
|
-
.dereference()
|
|
1255
|
-
.select('_id', 'title')
|
|
1256
|
-
.postFilter('count(lessons) > 10')
|
|
1257
|
-
.order('title asc')
|
|
1258
|
-
.slice(5, 15)
|
|
1259
|
-
.build()
|
|
1260
|
-
|
|
1261
|
-
const parts = [
|
|
1262
|
-
{ name: 'selector', value: '*[_type == "learning-path"]' },
|
|
1263
|
-
{ name: 'filter', value: '[published == true]' },
|
|
1264
|
-
{ name: 'accessor', value: '.courses[]->' },
|
|
1265
|
-
{ name: 'projection', value: '{ _id, title }' },
|
|
1266
|
-
{ name: 'postFilter', value: '[count(lessons) > 10]' },
|
|
1267
|
-
{ name: 'order', value: '| order(title asc)' },
|
|
1268
|
-
{ name: 'slice', value: '[5...20]' },
|
|
1269
|
-
]
|
|
1270
|
-
|
|
1271
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
1272
|
-
const currentIndex = result.indexOf(parts[i].value)
|
|
1273
|
-
const nextIndex = result.indexOf(parts[i + 1].value)
|
|
1274
|
-
expect(currentIndex).toBeLessThan(nextIndex)
|
|
1275
|
-
}
|
|
1276
|
-
})
|
|
1277
|
-
})
|
|
1278
|
-
|
|
1279
|
-
describe('Real-World Use Cases', () => {
|
|
1280
|
-
test('builds query accessing title from filtered courses', () => {
|
|
1281
|
-
const result = query()
|
|
1282
|
-
.and('_type == "course"')
|
|
1283
|
-
.and('published == true')
|
|
1284
|
-
.and('brand == "drumeo"')
|
|
1285
|
-
.access('title')
|
|
1286
|
-
.order('title asc')
|
|
1287
|
-
.slice(0, 50)
|
|
1288
|
-
.build()
|
|
1289
|
-
expect(result).toContain(
|
|
1290
|
-
'*[_type == "course" && published == true && brand == "drumeo"].title'
|
|
1291
|
-
)
|
|
1292
|
-
expect(result).toContain('| order(title asc)')
|
|
1293
|
-
expect(result).toContain('[0...50]')
|
|
1294
|
-
})
|
|
1295
|
-
|
|
1296
|
-
test('builds query dereferencing instructor reference', () => {
|
|
1297
|
-
const result = query()
|
|
1298
|
-
.and('_type == "course"')
|
|
1299
|
-
.and('published == true')
|
|
1300
|
-
.access('instructor')
|
|
1301
|
-
.dereference()
|
|
1302
|
-
.select(
|
|
1303
|
-
'_id',
|
|
1304
|
-
'name',
|
|
1305
|
-
'biography',
|
|
1306
|
-
'"courseCount": count(*[_type == "course" && references(^._id)])'
|
|
1307
|
-
)
|
|
1308
|
-
.order('name asc')
|
|
1309
|
-
.build()
|
|
1310
|
-
expect(result).toContain('*[_type == "course" && published == true].instructor->')
|
|
1311
|
-
expect(result).toContain(
|
|
1312
|
-
'{ _id, name, biography, "courseCount": count(*[_type == "course" && references(^._id)]) }'
|
|
1313
|
-
)
|
|
1314
|
-
expect(result).toContain('| order(name asc)')
|
|
1315
|
-
})
|
|
1316
|
-
|
|
1317
|
-
test('builds query accessing property then projecting fields', () => {
|
|
1318
|
-
const result = query()
|
|
1319
|
-
.and('_type == "learning-path-v2"')
|
|
1320
|
-
.and('slug.current == $slug')
|
|
1321
|
-
.first()
|
|
1322
|
-
.access('courses[]')
|
|
1323
|
-
.dereference()
|
|
1324
|
-
.select('_id', 'title', 'difficulty', '"lessonCount": count(lessons)')
|
|
1325
|
-
.order('order asc')
|
|
1326
|
-
.build()
|
|
1327
|
-
expect(result).toContain(
|
|
1328
|
-
'*[_type == "learning-path-v2" && slug.current == $slug].courses[]->'
|
|
1329
|
-
)
|
|
1330
|
-
expect(result).toContain('{ _id, title, difficulty, "lessonCount": count(lessons) }')
|
|
1331
|
-
expect(result).toContain('[0]')
|
|
1332
|
-
})
|
|
1333
|
-
|
|
1334
|
-
test('builds query accessing array elements', () => {
|
|
1335
|
-
const result = query()
|
|
1336
|
-
.and('_type == "pack"')
|
|
1337
|
-
.and('_id == $packId')
|
|
1338
|
-
.first()
|
|
1339
|
-
.access('contents[]')
|
|
1340
|
-
.select('_id', 'title', '_type')
|
|
1341
|
-
.build()
|
|
1342
|
-
expect(result).toContain('*[_type == "pack" && _id == $packId].contents[]')
|
|
1343
|
-
expect(result).toContain('{ _id, title, _type }')
|
|
1344
|
-
expect(result).toContain('[0]')
|
|
1345
|
-
})
|
|
1346
|
-
|
|
1347
|
-
test('builds query dereferencing array of references', () => {
|
|
1348
|
-
const result = query()
|
|
1349
|
-
.and('_type == "playlist"')
|
|
1350
|
-
.and('user._ref == $userId')
|
|
1351
|
-
.access('items[]')
|
|
1352
|
-
.dereference()
|
|
1353
|
-
.select('_id', 'title', '_type', 'publishedOn')
|
|
1354
|
-
.order('publishedOn desc')
|
|
1355
|
-
.build()
|
|
1356
|
-
expect(result).toContain('*[_type == "playlist" && user._ref == $userId].items[]->')
|
|
1357
|
-
expect(result).toContain('{ _id, title, _type, publishedOn }')
|
|
1358
|
-
})
|
|
1359
|
-
|
|
1360
|
-
test('builds query with accessor and post-filter aggregation', () => {
|
|
1361
|
-
const result = query()
|
|
1362
|
-
.and('_type == "learning-path"')
|
|
1363
|
-
.access('courses[]')
|
|
1364
|
-
.dereference()
|
|
1365
|
-
.select('_id', 'title', '"totalLessons": count(lessons)')
|
|
1366
|
-
.postFilter('totalLessons >= 10')
|
|
1367
|
-
.postFilter('totalLessons <= 50')
|
|
1368
|
-
.order('totalLessons desc')
|
|
1369
|
-
.slice(0, 20)
|
|
1370
|
-
.build()
|
|
1371
|
-
expect(result).toContain('*[_type == "learning-path"].courses[]->')
|
|
1372
|
-
expect(result).toContain('{ _id, title, "totalLessons": count(lessons) }')
|
|
1373
|
-
expect(result).toContain('[totalLessons >= 10 && totalLessons <= 50]')
|
|
1374
|
-
expect(result).toContain('| order(totalLessons desc)')
|
|
1375
|
-
expect(result).toContain('[0...20]')
|
|
1376
|
-
})
|
|
1377
|
-
|
|
1378
|
-
test('builds complex query with filters, accessor, projection, and ordering', () => {
|
|
1379
|
-
const result = query()
|
|
1380
|
-
.and('_type in ["course", "song", "play-along"]')
|
|
1381
|
-
.and('!(_id in path("drafts.**"))')
|
|
1382
|
-
.and('published == true')
|
|
1383
|
-
.or('difficulty == "beginner"', 'difficulty == "intermediate"')
|
|
1384
|
-
.and('brand == $brand')
|
|
1385
|
-
.access('instructor')
|
|
1386
|
-
.dereference()
|
|
1387
|
-
.select(
|
|
1388
|
-
'_id',
|
|
1389
|
-
'name',
|
|
1390
|
-
'biography',
|
|
1391
|
-
'"activeCoursesCount": count(*[_type == "course" && published == true && references(^._id)])'
|
|
1392
|
-
)
|
|
1393
|
-
.postFilter('activeCoursesCount > 0')
|
|
1394
|
-
.order('name asc')
|
|
1395
|
-
.slice(0, 30)
|
|
1396
|
-
.build()
|
|
1397
|
-
expect(result).toContain('*[_type in ["course", "song", "play-along"]')
|
|
1398
|
-
expect(result).toContain('!(_id in path("drafts.**"))')
|
|
1399
|
-
expect(result).toContain('published == true')
|
|
1400
|
-
expect(result).toContain('(difficulty == "beginner" || difficulty == "intermediate")')
|
|
1401
|
-
expect(result).toContain('brand == $brand')
|
|
1402
|
-
expect(result).toContain('.instructor->')
|
|
1403
|
-
expect(result).toContain('{ _id, name, biography')
|
|
1404
|
-
expect(result).toContain('[activeCoursesCount > 0]')
|
|
1405
|
-
expect(result).toContain('| order(name asc)')
|
|
1406
|
-
expect(result).toContain('[0...30]')
|
|
1407
|
-
})
|
|
1408
|
-
|
|
1409
|
-
test('builds query with accessor but no filters', () => {
|
|
1410
|
-
const result = query().access('metadata').select('author', 'publishedDate').build()
|
|
1411
|
-
expect(result).toContain('*[].metadata')
|
|
1412
|
-
expect(result).toContain('{ author, publishedDate }')
|
|
1413
|
-
})
|
|
1414
|
-
})
|
|
1415
|
-
|
|
1416
|
-
describe('Accessor Edge Cases', () => {
|
|
1417
|
-
test('handles empty string clearing accessor', () => {
|
|
1418
|
-
const builder = query().and('_type == "course"').access('title')
|
|
1419
|
-
expect(builder._state().accessor).toBe('.title')
|
|
1420
|
-
builder.access('')
|
|
1421
|
-
expect(builder._state().accessor).toBe('')
|
|
1422
|
-
const result = builder.build()
|
|
1423
|
-
expect(result).toBe('*[_type == "course"]')
|
|
1424
|
-
})
|
|
1425
|
-
|
|
1426
|
-
test('handles dereferencing with array notation', () => {
|
|
1427
|
-
const result = query().and('_type == "playlist"').access('items[]').dereference().build()
|
|
1428
|
-
expect(result).toBe('*[_type == "playlist"].items[]->')
|
|
1429
|
-
})
|
|
1430
|
-
|
|
1431
|
-
test('handles accessor without filters', () => {
|
|
1432
|
-
const result = query().access('field').build()
|
|
1433
|
-
expect(result).toBe('*[].field')
|
|
1434
|
-
})
|
|
1435
|
-
|
|
1436
|
-
test('handles multiple access() calls with different parameters', () => {
|
|
1437
|
-
const builder = query().and('_type == "course"')
|
|
1438
|
-
builder.access('field1')
|
|
1439
|
-
expect(builder._state().accessor).toBe('.field1')
|
|
1440
|
-
builder.access('field2')
|
|
1441
|
-
expect(builder._state().accessor).toBe('.field2')
|
|
1442
|
-
builder.access('field3').dereference()
|
|
1443
|
-
expect(builder._state().accessor).toBe('.field3')
|
|
1444
|
-
expect(builder._state().dereference).toBe('->')
|
|
1445
|
-
})
|
|
1446
|
-
|
|
1447
|
-
test('state reflects cleared accessor', () => {
|
|
1448
|
-
const builder = query().access('field').access('')
|
|
1449
|
-
expect(builder._state().accessor).toBe('')
|
|
1450
|
-
})
|
|
1451
|
-
|
|
1452
|
-
test('handles accessor with empty filter brackets', () => {
|
|
1453
|
-
const result = query().access('title').build()
|
|
1454
|
-
expect(result).toBe('*[].title')
|
|
1455
|
-
expect(result).toMatch(/^\*\[\]\.title$/)
|
|
1456
|
-
})
|
|
1457
|
-
})
|
|
1458
|
-
|
|
1459
|
-
describe('Accessor Default Parameter Behavior', () => {
|
|
1460
|
-
test('dereference parameter defaults to false when not provided', () => {
|
|
1461
|
-
const result1 = query().access('field').build()
|
|
1462
|
-
const result2 = query().access('field').build()
|
|
1463
|
-
expect(result1).toBe(result2)
|
|
1464
|
-
expect(result1).toBe('*[].field')
|
|
1465
|
-
expect(result1).not.toContain('->')
|
|
1466
|
-
})
|
|
1467
|
-
})
|
|
1468
|
-
})
|
|
1469
|
-
})
|