musora-content-services 2.94.8 → 2.95.0
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 +16 -0
- package/CLAUDE.md +408 -0
- package/babel.config.cjs +10 -0
- package/jsdoc.json +2 -1
- package/package.json +2 -2
- package/src/constants/award-assets.js +35 -0
- package/src/filterBuilder.js +7 -2
- package/src/index.d.ts +26 -5
- package/src/index.js +26 -5
- package/src/services/awards/award-callbacks.js +126 -0
- package/src/services/awards/award-query.js +327 -0
- package/src/services/awards/internal/.indexignore +1 -0
- package/src/services/awards/internal/award-definitions.js +239 -0
- package/src/services/awards/internal/award-events.js +102 -0
- package/src/services/awards/internal/award-manager.js +162 -0
- package/src/services/awards/internal/certificate-builder.js +66 -0
- package/src/services/awards/internal/completion-data-generator.js +84 -0
- package/src/services/awards/internal/content-progress-observer.js +137 -0
- package/src/services/awards/internal/image-utils.js +62 -0
- package/src/services/awards/internal/message-generator.js +17 -0
- package/src/services/awards/internal/types.js +5 -0
- package/src/services/awards/types.d.ts +79 -0
- package/src/services/awards/types.js +101 -0
- package/src/services/config.js +24 -4
- package/src/services/content-org/learning-paths.ts +19 -15
- package/src/services/gamification/awards.ts +114 -83
- package/src/services/progress-events.js +58 -0
- package/src/services/progress-row/method-card.js +20 -5
- package/src/services/sanity.js +1 -1
- package/src/services/sync/fetch.ts +10 -2
- package/src/services/sync/manager.ts +6 -0
- package/src/services/sync/models/ContentProgress.ts +5 -6
- package/src/services/sync/models/UserAwardProgress.ts +55 -0
- package/src/services/sync/models/index.ts +1 -0
- package/src/services/sync/repositories/content-progress.ts +47 -25
- package/src/services/sync/repositories/index.ts +1 -0
- package/src/services/sync/repositories/practices.ts +16 -1
- package/src/services/sync/repositories/user-award-progress.ts +133 -0
- package/src/services/sync/repository-proxy.ts +6 -0
- package/src/services/sync/retry.ts +12 -11
- package/src/services/sync/schema/index.ts +18 -3
- package/src/services/sync/store/index.ts +53 -8
- package/src/services/sync/store/push-coalescer.ts +3 -3
- package/src/services/sync/store-configs.ts +7 -1
- package/src/services/userActivity.js +0 -1
- package/test/HttpClient.test.js +6 -6
- package/test/awards/award-alacarte-observer.test.js +196 -0
- package/test/awards/award-auto-refresh.test.js +83 -0
- package/test/awards/award-calculations.test.js +33 -0
- package/test/awards/award-certificate-display.test.js +328 -0
- package/test/awards/award-collection-edge-cases.test.js +210 -0
- package/test/awards/award-collection-filtering.test.js +285 -0
- package/test/awards/award-completion-flow.test.js +213 -0
- package/test/awards/award-exclusion-handling.test.js +273 -0
- package/test/awards/award-multi-lesson.test.js +241 -0
- package/test/awards/award-observer-integration.test.js +325 -0
- package/test/awards/award-query-messages.test.js +438 -0
- package/test/awards/award-user-collection.test.js +412 -0
- package/test/awards/duplicate-prevention.test.js +118 -0
- package/test/awards/helpers/completion-mock.js +54 -0
- package/test/awards/helpers/index.js +3 -0
- package/test/awards/helpers/mock-setup.js +69 -0
- package/test/awards/helpers/progress-emitter.js +39 -0
- package/test/awards/message-generator.test.js +162 -0
- package/test/initializeTests.js +6 -0
- package/test/mockData/award-definitions.js +171 -0
- package/test/sync/models/award-database-integration.test.js +519 -0
- package/tools/generate-index.cjs +9 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import { Database } from '@nozbe/watermelondb'
|
|
2
|
+
import LokiJSAdapter from '@nozbe/watermelondb/adapters/lokijs'
|
|
3
|
+
import { Q } from '@nozbe/watermelondb'
|
|
4
|
+
import schema, { SYNC_TABLES } from '../../../src/services/sync/schema'
|
|
5
|
+
import ContentProgress from '../../../src/services/sync/models/ContentProgress'
|
|
6
|
+
import Practice from '../../../src/services/sync/models/Practice'
|
|
7
|
+
import ContentLike from '../../../src/services/sync/models/ContentLike'
|
|
8
|
+
import UserAwardProgress from '../../../src/services/sync/models/UserAwardProgress'
|
|
9
|
+
|
|
10
|
+
describe('Award System - Direct Database Integration', () => {
|
|
11
|
+
let database
|
|
12
|
+
let progressCollection
|
|
13
|
+
let practiceCollection
|
|
14
|
+
let awardProgressCollection
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
const adapter = new LokiJSAdapter({
|
|
18
|
+
schema,
|
|
19
|
+
useWebWorker: false,
|
|
20
|
+
useIncrementalIndexedDB: false,
|
|
21
|
+
dbName: `test_${Date.now()}_${Math.random()}`,
|
|
22
|
+
extraLokiOptions: {
|
|
23
|
+
autosave: false
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
database = new Database({
|
|
28
|
+
adapter,
|
|
29
|
+
modelClasses: [ContentProgress, Practice, ContentLike, UserAwardProgress]
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
progressCollection = database.collections.get(SYNC_TABLES.CONTENT_PROGRESS)
|
|
33
|
+
practiceCollection = database.collections.get(SYNC_TABLES.PRACTICES)
|
|
34
|
+
awardProgressCollection = database.collections.get(SYNC_TABLES.USER_AWARD_PROGRESS)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
if (database) {
|
|
39
|
+
try {
|
|
40
|
+
await database.write(async () => {
|
|
41
|
+
await database.unsafeResetDatabase()
|
|
42
|
+
})
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error resetting database:', error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('ContentProgress CRUD Operations', () => {
|
|
50
|
+
test('creates a progress record with started state', async () => {
|
|
51
|
+
const now = Math.floor(Date.now() / 1000)
|
|
52
|
+
|
|
53
|
+
await database.write(async () => {
|
|
54
|
+
await progressCollection.create(record => {
|
|
55
|
+
record._raw.id = '417045'
|
|
56
|
+
record._raw.content_id = 417045
|
|
57
|
+
record._raw.content_brand = 'drumeo'
|
|
58
|
+
record._raw.state = 'started'
|
|
59
|
+
record._raw.progress_percent = 50
|
|
60
|
+
record._raw.resume_time_seconds = 120
|
|
61
|
+
record._raw.created_at = now
|
|
62
|
+
record._raw.updated_at = now
|
|
63
|
+
record._raw._status = 'synced'
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const record = await progressCollection.find('417045')
|
|
68
|
+
expect(record.content_id).toBe(417045)
|
|
69
|
+
expect(record.state).toBe('started')
|
|
70
|
+
expect(record.progress_percent).toBe(50)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('updates progress record to completed state', async () => {
|
|
74
|
+
const now = Math.floor(Date.now() / 1000)
|
|
75
|
+
|
|
76
|
+
await database.write(async () => {
|
|
77
|
+
await progressCollection.create(record => {
|
|
78
|
+
record._raw.id = '417045'
|
|
79
|
+
record._raw.content_id = 417045
|
|
80
|
+
record._raw.content_brand = 'drumeo'
|
|
81
|
+
record._raw.state = 'started'
|
|
82
|
+
record._raw.progress_percent = 50
|
|
83
|
+
record._raw.resume_time_seconds = 0
|
|
84
|
+
record._raw.created_at = now
|
|
85
|
+
record._raw.updated_at = now
|
|
86
|
+
record._raw._status = 'synced'
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await database.write(async () => {
|
|
91
|
+
const record = await progressCollection.find('417045')
|
|
92
|
+
await record.update(r => {
|
|
93
|
+
r._raw.state = 'completed'
|
|
94
|
+
r._raw.progress_percent = 100
|
|
95
|
+
r._raw.updated_at = now + 100
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const updated = await progressCollection.find('417045')
|
|
100
|
+
expect(updated.state).toBe('completed')
|
|
101
|
+
expect(updated.progress_percent).toBe(100)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('queries multiple completed lessons', async () => {
|
|
105
|
+
const now = Math.floor(Date.now() / 1000)
|
|
106
|
+
const lessonIds = [417045, 417046, 417047]
|
|
107
|
+
|
|
108
|
+
await database.write(async () => {
|
|
109
|
+
for (const lessonId of lessonIds) {
|
|
110
|
+
await progressCollection.create(record => {
|
|
111
|
+
record._raw.id = `${lessonId}`
|
|
112
|
+
record._raw.content_id = lessonId
|
|
113
|
+
record._raw.content_brand = 'drumeo'
|
|
114
|
+
record._raw.state = 'completed'
|
|
115
|
+
record._raw.progress_percent = 100
|
|
116
|
+
record._raw.resume_time_seconds = 0
|
|
117
|
+
record._raw.created_at = now
|
|
118
|
+
record._raw.updated_at = now
|
|
119
|
+
record._raw._status = 'synced'
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const completed = await progressCollection
|
|
125
|
+
.query(Q.where('state', 'completed'))
|
|
126
|
+
.fetch()
|
|
127
|
+
|
|
128
|
+
expect(completed.length).toBe(3)
|
|
129
|
+
expect(completed.map(r => r.content_id)).toEqual(expect.arrayContaining(lessonIds))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('filters by content_id', async () => {
|
|
133
|
+
const now = Math.floor(Date.now() / 1000)
|
|
134
|
+
|
|
135
|
+
await database.write(async () => {
|
|
136
|
+
await progressCollection.create(record => {
|
|
137
|
+
record._raw.id = '417045'
|
|
138
|
+
record._raw.content_id = 417045
|
|
139
|
+
record._raw.content_brand = 'drumeo'
|
|
140
|
+
record._raw.state = 'completed'
|
|
141
|
+
record._raw.progress_percent = 100
|
|
142
|
+
record._raw.resume_time_seconds = 0
|
|
143
|
+
record._raw.created_at = now
|
|
144
|
+
record._raw.updated_at = now
|
|
145
|
+
record._raw._status = 'synced'
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const results = await progressCollection
|
|
150
|
+
.query(Q.where('content_id', 417045))
|
|
151
|
+
.fetch()
|
|
152
|
+
|
|
153
|
+
expect(results.length).toBe(1)
|
|
154
|
+
expect(results[0].content_id).toBe(417045)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('ContentPractice Operations', () => {
|
|
159
|
+
test('creates practice session record', async () => {
|
|
160
|
+
const now = Math.floor(Date.now() / 1000)
|
|
161
|
+
|
|
162
|
+
await database.write(async () => {
|
|
163
|
+
await practiceCollection.create(record => {
|
|
164
|
+
record._raw.id = `practice_417045_${now}`
|
|
165
|
+
record._raw.content_id = 417045
|
|
166
|
+
record._raw.duration_seconds = 1200
|
|
167
|
+
record._raw.created_at = now
|
|
168
|
+
record._raw.updated_at = now
|
|
169
|
+
record._raw._status = 'synced'
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const records = await practiceCollection
|
|
174
|
+
.query(Q.where('content_id', 417045))
|
|
175
|
+
.fetch()
|
|
176
|
+
|
|
177
|
+
expect(records.length).toBe(1)
|
|
178
|
+
expect(records[0].duration_seconds).toBe(1200)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('queries practice sessions for multiple lessons', async () => {
|
|
182
|
+
const now = Math.floor(Date.now() / 1000)
|
|
183
|
+
const lessonIds = [417045, 417046, 417047]
|
|
184
|
+
|
|
185
|
+
await database.write(async () => {
|
|
186
|
+
for (const lessonId of lessonIds) {
|
|
187
|
+
await practiceCollection.create(record => {
|
|
188
|
+
record._raw.id = `practice_${lessonId}_${now}`
|
|
189
|
+
record._raw.content_id = lessonId
|
|
190
|
+
record._raw.duration_seconds = 600
|
|
191
|
+
record._raw.created_at = now
|
|
192
|
+
record._raw.updated_at = now
|
|
193
|
+
record._raw._status = 'synced'
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const allPractices = await practiceCollection
|
|
199
|
+
.query(Q.where('content_id', Q.oneOf(lessonIds)))
|
|
200
|
+
.fetch()
|
|
201
|
+
|
|
202
|
+
expect(allPractices.length).toBe(3)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('calculates total practice time for lessons', async () => {
|
|
206
|
+
const now = Math.floor(Date.now() / 1000)
|
|
207
|
+
const lessonId = 417045
|
|
208
|
+
|
|
209
|
+
await database.write(async () => {
|
|
210
|
+
await practiceCollection.create(record => {
|
|
211
|
+
record._raw.id = `practice_${lessonId}_1`
|
|
212
|
+
record._raw.content_id = lessonId
|
|
213
|
+
record._raw.duration_seconds = 600
|
|
214
|
+
record._raw.created_at = now
|
|
215
|
+
record._raw.updated_at = now
|
|
216
|
+
record._raw._status = 'synced'
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
await practiceCollection.create(record => {
|
|
220
|
+
record._raw.id = `practice_${lessonId}_2`
|
|
221
|
+
record._raw.content_id = lessonId
|
|
222
|
+
record._raw.duration_seconds = 900
|
|
223
|
+
record._raw.created_at = now + 3600
|
|
224
|
+
record._raw.updated_at = now + 3600
|
|
225
|
+
record._raw._status = 'synced'
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const practices = await practiceCollection
|
|
230
|
+
.query(Q.where('content_id', lessonId))
|
|
231
|
+
.fetch()
|
|
232
|
+
|
|
233
|
+
const totalSeconds = practices.reduce((sum, p) => sum + p.duration_seconds, 0)
|
|
234
|
+
const totalMinutes = Math.round(totalSeconds / 60)
|
|
235
|
+
|
|
236
|
+
expect(totalMinutes).toBe(25)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('UserAwardProgress Operations', () => {
|
|
241
|
+
test('creates award progress record with 0%', async () => {
|
|
242
|
+
const now = Math.floor(Date.now() / 1000)
|
|
243
|
+
const awardId = 'test-award-123'
|
|
244
|
+
|
|
245
|
+
await database.write(async () => {
|
|
246
|
+
await awardProgressCollection.create(record => {
|
|
247
|
+
record._raw.id = awardId
|
|
248
|
+
record._raw.award_id = awardId
|
|
249
|
+
record._raw.progress_percentage = 0
|
|
250
|
+
record._raw.completed_at = null
|
|
251
|
+
record._raw.progress_data = null
|
|
252
|
+
record._raw.completion_data = null
|
|
253
|
+
record._raw.created_at = now
|
|
254
|
+
record._raw.updated_at = now
|
|
255
|
+
record._raw._status = 'synced'
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const record = await awardProgressCollection.find(awardId)
|
|
260
|
+
expect(record.progress_percentage).toBe(0)
|
|
261
|
+
expect(record.completed_at).toBeNull()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('updates award progress with progress_data', async () => {
|
|
265
|
+
const now = Math.floor(Date.now() / 1000)
|
|
266
|
+
const awardId = 'test-award-123'
|
|
267
|
+
|
|
268
|
+
await database.write(async () => {
|
|
269
|
+
await awardProgressCollection.create(record => {
|
|
270
|
+
record._raw.id = awardId
|
|
271
|
+
record._raw.award_id = awardId
|
|
272
|
+
record._raw.progress_percentage = 0
|
|
273
|
+
record._raw.completed_at = null
|
|
274
|
+
record._raw.progress_data = null
|
|
275
|
+
record._raw.completion_data = null
|
|
276
|
+
record._raw.created_at = now
|
|
277
|
+
record._raw.updated_at = now
|
|
278
|
+
record._raw._status = 'synced'
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const progressData = {
|
|
283
|
+
completedLessonIds: [417045, 417046],
|
|
284
|
+
totalLessons: 4,
|
|
285
|
+
completedCount: 2
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await database.write(async () => {
|
|
289
|
+
const record = await awardProgressCollection.find(awardId)
|
|
290
|
+
await record.update(r => {
|
|
291
|
+
r._raw.progress_percentage = 50
|
|
292
|
+
r._raw.progress_data = JSON.stringify(progressData)
|
|
293
|
+
r._raw.updated_at = now + 100
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const updated = await awardProgressCollection.find(awardId)
|
|
298
|
+
expect(updated.progress_percentage).toBe(50)
|
|
299
|
+
expect(updated.progress_data).toEqual(progressData)
|
|
300
|
+
expect(updated.progress_data.completedLessonIds).toEqual([417045, 417046])
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('completes award with completion_data', async () => {
|
|
304
|
+
const now = Math.floor(Date.now() / 1000)
|
|
305
|
+
const awardId = 'test-award-123'
|
|
306
|
+
|
|
307
|
+
await database.write(async () => {
|
|
308
|
+
await awardProgressCollection.create(record => {
|
|
309
|
+
record._raw.id = awardId
|
|
310
|
+
record._raw.award_id = awardId
|
|
311
|
+
record._raw.progress_percentage = 50
|
|
312
|
+
record._raw.completed_at = null
|
|
313
|
+
record._raw.progress_data = JSON.stringify({
|
|
314
|
+
completedLessonIds: [417045, 417046],
|
|
315
|
+
totalLessons: 4,
|
|
316
|
+
completedCount: 2
|
|
317
|
+
})
|
|
318
|
+
record._raw.completion_data = null
|
|
319
|
+
record._raw.created_at = now
|
|
320
|
+
record._raw.updated_at = now
|
|
321
|
+
record._raw._status = 'synced'
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const completionData = {
|
|
326
|
+
content_title: 'Blues Foundations',
|
|
327
|
+
completed_at: new Date().toISOString(),
|
|
328
|
+
days_user_practiced: 7,
|
|
329
|
+
practice_minutes: 180
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
await database.write(async () => {
|
|
333
|
+
const record = await awardProgressCollection.find(awardId)
|
|
334
|
+
await record.update(r => {
|
|
335
|
+
r._raw.progress_percentage = 100
|
|
336
|
+
r._raw.completed_at = now + 200
|
|
337
|
+
r._raw.completion_data = JSON.stringify(completionData)
|
|
338
|
+
r._raw.updated_at = now + 200
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const completed = await awardProgressCollection.find(awardId)
|
|
343
|
+
expect(completed.progress_percentage).toBe(100)
|
|
344
|
+
expect(completed.completed_at).toBe(now + 200)
|
|
345
|
+
expect(completed.completion_data).toEqual(completionData)
|
|
346
|
+
expect(completed.isCompleted).toBe(true)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('queries in-progress awards', async () => {
|
|
350
|
+
const now = Math.floor(Date.now() / 1000)
|
|
351
|
+
|
|
352
|
+
await database.write(async () => {
|
|
353
|
+
await awardProgressCollection.create(record => {
|
|
354
|
+
record._raw.id = 'award-1'
|
|
355
|
+
record._raw.award_id = 'award-1'
|
|
356
|
+
record._raw.progress_percentage = 50
|
|
357
|
+
record._raw.completed_at = null
|
|
358
|
+
record._raw.progress_data = null
|
|
359
|
+
record._raw.completion_data = null
|
|
360
|
+
record._raw.created_at = now
|
|
361
|
+
record._raw.updated_at = now
|
|
362
|
+
record._raw._status = 'synced'
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
await awardProgressCollection.create(record => {
|
|
366
|
+
record._raw.id = 'award-2'
|
|
367
|
+
record._raw.award_id = 'award-2'
|
|
368
|
+
record._raw.progress_percentage = 100
|
|
369
|
+
record._raw.completed_at = now
|
|
370
|
+
record._raw.progress_data = null
|
|
371
|
+
record._raw.completion_data = JSON.stringify({})
|
|
372
|
+
record._raw.created_at = now
|
|
373
|
+
record._raw.updated_at = now
|
|
374
|
+
record._raw._status = 'synced'
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const inProgress = await awardProgressCollection
|
|
379
|
+
.query(
|
|
380
|
+
Q.where('progress_percentage', Q.gt(0)),
|
|
381
|
+
Q.where('completed_at', Q.eq(null))
|
|
382
|
+
)
|
|
383
|
+
.fetch()
|
|
384
|
+
|
|
385
|
+
expect(inProgress.length).toBe(1)
|
|
386
|
+
expect(inProgress[0].award_id).toBe('award-1')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('queries completed awards', async () => {
|
|
390
|
+
const now = Math.floor(Date.now() / 1000)
|
|
391
|
+
|
|
392
|
+
await database.write(async () => {
|
|
393
|
+
await awardProgressCollection.create(record => {
|
|
394
|
+
record._raw.id = 'award-1'
|
|
395
|
+
record._raw.award_id = 'award-1'
|
|
396
|
+
record._raw.progress_percentage = 100
|
|
397
|
+
record._raw.completed_at = now
|
|
398
|
+
record._raw.progress_data = null
|
|
399
|
+
record._raw.completion_data = JSON.stringify({})
|
|
400
|
+
record._raw.created_at = now
|
|
401
|
+
record._raw.updated_at = now
|
|
402
|
+
record._raw._status = 'synced'
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await awardProgressCollection.create(record => {
|
|
406
|
+
record._raw.id = 'award-2'
|
|
407
|
+
record._raw.award_id = 'award-2'
|
|
408
|
+
record._raw.progress_percentage = 50
|
|
409
|
+
record._raw.completed_at = null
|
|
410
|
+
record._raw.progress_data = null
|
|
411
|
+
record._raw.completion_data = null
|
|
412
|
+
record._raw.created_at = now
|
|
413
|
+
record._raw.updated_at = now
|
|
414
|
+
record._raw._status = 'synced'
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const completed = await awardProgressCollection
|
|
419
|
+
.query(Q.where('completed_at', Q.notEq(null)))
|
|
420
|
+
.fetch()
|
|
421
|
+
|
|
422
|
+
expect(completed.length).toBe(1)
|
|
423
|
+
expect(completed[0].award_id).toBe('award-1')
|
|
424
|
+
expect(completed[0].isCompleted).toBe(true)
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
describe('Cross-Collection Queries', () => {
|
|
429
|
+
test('finds progress and practice for same lesson', async () => {
|
|
430
|
+
const now = Math.floor(Date.now() / 1000)
|
|
431
|
+
const lessonId = 417045
|
|
432
|
+
|
|
433
|
+
await database.write(async () => {
|
|
434
|
+
await progressCollection.create(record => {
|
|
435
|
+
record._raw.id = `${lessonId}`
|
|
436
|
+
record._raw.content_id = lessonId
|
|
437
|
+
record._raw.content_brand = 'drumeo'
|
|
438
|
+
record._raw.state = 'completed'
|
|
439
|
+
record._raw.progress_percent = 100
|
|
440
|
+
record._raw.resume_time_seconds = 0
|
|
441
|
+
record._raw.created_at = now
|
|
442
|
+
record._raw.updated_at = now
|
|
443
|
+
record._raw._status = 'synced'
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
await practiceCollection.create(record => {
|
|
447
|
+
record._raw.id = `practice_${lessonId}`
|
|
448
|
+
record._raw.content_id = lessonId
|
|
449
|
+
record._raw.duration_seconds = 1200
|
|
450
|
+
record._raw.created_at = now
|
|
451
|
+
record._raw.updated_at = now
|
|
452
|
+
record._raw._status = 'synced'
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const progress = await progressCollection.find(`${lessonId}`)
|
|
457
|
+
const practices = await practiceCollection
|
|
458
|
+
.query(Q.where('content_id', lessonId))
|
|
459
|
+
.fetch()
|
|
460
|
+
|
|
461
|
+
expect(progress.state).toBe('completed')
|
|
462
|
+
expect(practices.length).toBe(1)
|
|
463
|
+
expect(practices[0].duration_seconds).toBe(1200)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('simulates award progress flow: lessons → award progress', async () => {
|
|
467
|
+
const now = Math.floor(Date.now() / 1000)
|
|
468
|
+
const awardId = 'test-award-123'
|
|
469
|
+
const lessonIds = [417045, 417046, 417047, 417048]
|
|
470
|
+
|
|
471
|
+
await database.write(async () => {
|
|
472
|
+
for (const lessonId of lessonIds.slice(0, 2)) {
|
|
473
|
+
await progressCollection.create(record => {
|
|
474
|
+
record._raw.id = `${lessonId}`
|
|
475
|
+
record._raw.content_id = lessonId
|
|
476
|
+
record._raw.content_brand = 'drumeo'
|
|
477
|
+
record._raw.state = 'completed'
|
|
478
|
+
record._raw.progress_percent = 100
|
|
479
|
+
record._raw.resume_time_seconds = 0
|
|
480
|
+
record._raw.created_at = now
|
|
481
|
+
record._raw.updated_at = now
|
|
482
|
+
record._raw._status = 'synced'
|
|
483
|
+
})
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const completedLessons = await progressCollection
|
|
488
|
+
.query(
|
|
489
|
+
Q.where('content_id', Q.oneOf(lessonIds)),
|
|
490
|
+
Q.where('state', 'completed')
|
|
491
|
+
)
|
|
492
|
+
.fetch()
|
|
493
|
+
|
|
494
|
+
const progressPercentage = Math.round((completedLessons.length / lessonIds.length) * 100)
|
|
495
|
+
|
|
496
|
+
await database.write(async () => {
|
|
497
|
+
await awardProgressCollection.create(record => {
|
|
498
|
+
record._raw.id = awardId
|
|
499
|
+
record._raw.award_id = awardId
|
|
500
|
+
record._raw.progress_percentage = progressPercentage
|
|
501
|
+
record._raw.completed_at = null
|
|
502
|
+
record._raw.progress_data = JSON.stringify({
|
|
503
|
+
completedLessonIds: completedLessons.map(l => l.content_id),
|
|
504
|
+
totalLessons: lessonIds.length,
|
|
505
|
+
completedCount: completedLessons.length
|
|
506
|
+
})
|
|
507
|
+
record._raw.completion_data = null
|
|
508
|
+
record._raw.created_at = now
|
|
509
|
+
record._raw.updated_at = now
|
|
510
|
+
record._raw._status = 'synced'
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const award = await awardProgressCollection.find(awardId)
|
|
515
|
+
expect(award.progress_percentage).toBe(50)
|
|
516
|
+
expect(award.progress_data.completedCount).toBe(2)
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
})
|
package/tools/generate-index.cjs
CHANGED
|
@@ -87,6 +87,15 @@ treeElements.forEach((treeNode) => {
|
|
|
87
87
|
const subDir = fs.readdirSync(filePath)
|
|
88
88
|
subDir.forEach((subFile) => {
|
|
89
89
|
const subFilePath = path.join(servicesDir, treeNode, subFile)
|
|
90
|
+
|
|
91
|
+
// Skip directories and check for .indexignore in nested directories
|
|
92
|
+
if (fs.lstatSync(subFilePath).isDirectory()) {
|
|
93
|
+
if (fs.existsSync(path.join(subFilePath, '.indexignore'))) {
|
|
94
|
+
console.log(`Skipping nested directory: ${treeNode}/${subFile} due to .indexignore`)
|
|
95
|
+
}
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
90
99
|
addFunctionsToFileExports(subFilePath, treeNode + '/' + subFile)
|
|
91
100
|
})
|
|
92
101
|
}
|