musora-content-services 2.160.4 → 2.161.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.agent/decisions/2026-05-20-live-event-fetch-permissions-id.md +23 -0
  2. package/CHANGELOG.md +19 -0
  3. package/package.json +1 -1
  4. package/src/contentTypeConfig.js +13 -15
  5. package/src/index.d.ts +6 -2
  6. package/src/index.js +6 -2
  7. package/src/infrastructure/sanity/README.md +230 -0
  8. package/src/infrastructure/sanity/SanityClient.ts +105 -0
  9. package/src/infrastructure/sanity/clients/ContentClient.ts +164 -0
  10. package/src/infrastructure/sanity/examples/usage.ts +101 -0
  11. package/src/infrastructure/sanity/executors/FetchQueryExecutor.ts +110 -0
  12. package/src/infrastructure/sanity/index.ts +19 -0
  13. package/src/infrastructure/sanity/interfaces/ConfigProvider.ts +6 -0
  14. package/src/infrastructure/sanity/interfaces/FetchByIdOptions.ts +7 -0
  15. package/src/infrastructure/sanity/interfaces/QueryExecutor.ts +8 -0
  16. package/src/infrastructure/sanity/interfaces/SanityConfig.ts +10 -0
  17. package/src/infrastructure/sanity/interfaces/SanityError.ts +7 -0
  18. package/src/infrastructure/sanity/interfaces/SanityQuery.ts +5 -0
  19. package/src/infrastructure/sanity/interfaces/SanityResponse.ts +6 -0
  20. package/src/infrastructure/sanity/providers/DefaultConfigProvider.ts +38 -0
  21. package/src/lib/sanity/decorators/base.ts +142 -0
  22. package/src/lib/sanity/decorators/examples.ts +229 -0
  23. package/src/lib/sanity/decorators/navigate-to.ts +139 -0
  24. package/src/lib/sanity/decorators/need-access.ts +40 -0
  25. package/src/lib/sanity/decorators/page-type.ts +35 -0
  26. package/src/services/awards/award-query.js +71 -0
  27. package/src/services/contentAggregator.js +1 -1
  28. package/src/services/multi-user-accounts/multi-user-accounts.ts +11 -7
  29. package/src/services/user/memberships.ts +46 -34
  30. package/src/services/user/profile.ts +66 -0
  31. package/test/unit/infrastructure/sanity/ContentClient.test.ts +168 -0
  32. package/test/unit/infrastructure/sanity/DefaultConfigProvider.test.ts +93 -0
  33. package/test/unit/infrastructure/sanity/FetchQueryExecutor.test.ts +174 -0
  34. package/test/unit/infrastructure/sanity/SanityClient.test.ts +140 -0
  35. package/test/unit/lib/sanity/decorators/base.test.ts +368 -0
  36. package/test/unit/lib/sanity/decorators/navigate-to.test.ts +266 -0
  37. package/test/unit/lib/sanity/decorators/need-access.test.ts +89 -0
  38. package/test/unit/lib/sanity/decorators/page-type.test.ts +81 -0
  39. package/.claude/settings.local.json +0 -23
  40. package/src/services/user/profile.js +0 -43
@@ -0,0 +1,229 @@
1
+ import {
2
+ decorateAll,
3
+ decorateAllAsync,
4
+ decorateAsync,
5
+ type FieldDecorator,
6
+ type FieldDecoratorAsync,
7
+ } from './base'
8
+ import { accessDecorator, decorateAccess, type AccessDecoratable } from './need-access'
9
+ import { pageTypeDecorator, decoratePageType, type PageTypeDecoratable } from './page-type'
10
+ import {
11
+ decorateNavigateTo,
12
+ navigateToDecorator,
13
+ WithNavigateTo,
14
+ type NavigateToDecoratable,
15
+ } from './navigate-to'
16
+ import type { UserPermissions } from '../../../services/permissions'
17
+
18
+ interface ContentRow extends AccessDecoratable, PageTypeDecoratable {
19
+ id: number
20
+ type?: string
21
+ permission_id?: number[]
22
+ children?: ContentRow[]
23
+ }
24
+
25
+ const perms: UserPermissions = {
26
+ permissions: [78, 91],
27
+ isAdmin: false,
28
+ isModerator: false,
29
+ isABasicMember: true,
30
+ }
31
+
32
+ const rows: ContentRow[] = [
33
+ {
34
+ id: 1,
35
+ type: 'course',
36
+ permission_id: [78],
37
+ children: [
38
+ { id: 2, type: 'song', permission_id: [91] },
39
+ { id: 3, type: 'play-along' },
40
+ ],
41
+ },
42
+ ]
43
+
44
+ export function singleDecoratorViaWrapper() {
45
+ const decorated = decorateAccess(rows, perms)
46
+ decorated[0].need_access satisfies boolean
47
+ return decorated
48
+ }
49
+
50
+ export function chainedWrappers() {
51
+ const withAccess = decorateAccess(rows, perms)
52
+ const withBoth = decoratePageType(withAccess)
53
+ withBoth[0].need_access satisfies boolean
54
+ withBoth[0].page_type satisfies 'song' | 'lesson'
55
+ return withBoth
56
+ }
57
+
58
+ export function composedSingleWalk() {
59
+ type Composed = AccessDecoratable & PageTypeDecoratable
60
+ const decorators: FieldDecorator<Composed>[] = [
61
+ accessDecorator(perms) as FieldDecorator<Composed>,
62
+ pageTypeDecorator as FieldDecorator<Composed>,
63
+ ]
64
+ return decorateAll(rows, decorators)
65
+ }
66
+
67
+ export function conditionalComposition(opts: { withAccess: boolean; withPageType: boolean }) {
68
+ type Composed = AccessDecoratable & PageTypeDecoratable
69
+ const decorators: FieldDecorator<Composed>[] = []
70
+ if (opts.withAccess) {
71
+ decorators.push(accessDecorator(perms) as FieldDecorator<Composed>)
72
+ }
73
+ if (opts.withPageType) {
74
+ decorators.push(pageTypeDecorator as FieldDecorator<Composed>)
75
+ }
76
+ return decorateAll(rows, decorators)
77
+ }
78
+
79
+ interface ProgressDecoratable extends AccessDecoratable {
80
+ progress_percent?: number
81
+ is_liked?: boolean
82
+ }
83
+
84
+ async function fetchProgress(id: number): Promise<number> {
85
+ return id * 10
86
+ }
87
+
88
+ async function fetchLiked(id: number): Promise<boolean> {
89
+ return id % 2 === 0
90
+ }
91
+
92
+ export async function singleAsyncDecorator() {
93
+ const decorated = (await decorateAsync(rows, 'progress_percent', (item) =>
94
+ fetchProgress(item.id as number)
95
+ )) as ProgressDecoratable[]
96
+ decorated[0].progress_percent satisfies number | undefined
97
+ return decorated
98
+ }
99
+
100
+ export async function parallelAsyncDecorators() {
101
+ const decorators: FieldDecoratorAsync<ProgressDecoratable>[] = [
102
+ {
103
+ field: 'progress_percent',
104
+ compute: (item) => fetchProgress(item.id as number),
105
+ },
106
+ {
107
+ field: 'is_liked',
108
+ compute: (item) => fetchLiked(item.id as number),
109
+ },
110
+ ]
111
+ const decorated = (await decorateAllAsync(
112
+ rows as ProgressDecoratable[],
113
+ decorators
114
+ )) as ProgressDecoratable[]
115
+ decorated[0].progress_percent satisfies number | undefined
116
+ decorated[0].is_liked satisfies boolean | undefined
117
+ return decorated
118
+ }
119
+
120
+ export async function mixedSyncThenAsync() {
121
+ const withAccess = decorateAll(rows, [
122
+ accessDecorator(perms) as FieldDecorator<ProgressDecoratable>,
123
+ ]) as ProgressDecoratable[]
124
+ return decorateAllAsync(withAccess, [
125
+ {
126
+ field: 'progress_percent',
127
+ compute: (item) => fetchProgress(item.id as number),
128
+ },
129
+ {
130
+ field: 'is_liked',
131
+ compute: (item) => fetchLiked(item.id as number),
132
+ },
133
+ ])
134
+ }
135
+
136
+ const courseLesson = (id: number): NavigateToDecoratable => ({
137
+ id,
138
+ type: 'course-lesson',
139
+ brand: 'drumeo',
140
+ thumbnail: '',
141
+ published_on: null,
142
+ status: 'published',
143
+ })
144
+
145
+ const navigateRows: NavigateToDecoratable[] = [
146
+ {
147
+ id: 1,
148
+ type: 'course',
149
+ brand: 'drumeo',
150
+ thumbnail: '',
151
+ published_on: null,
152
+ status: 'published',
153
+ children: [courseLesson(101), courseLesson(102), courseLesson(103)],
154
+ },
155
+ {
156
+ id: 2,
157
+ type: 'course-collection',
158
+ brand: 'drumeo',
159
+ thumbnail: '',
160
+ published_on: null,
161
+ status: 'published',
162
+ children: [
163
+ {
164
+ id: 201,
165
+ type: 'course',
166
+ brand: 'drumeo',
167
+ thumbnail: '',
168
+ published_on: null,
169
+ status: 'published',
170
+ children: [courseLesson(301), courseLesson(302)],
171
+ },
172
+ ],
173
+ },
174
+ ]
175
+
176
+ export async function singleAsyncNavigateTo() {
177
+ const decorated = await decorateNavigateTo(navigateRows)
178
+ void decorated[0].navigateTo
179
+ void decorated[1].navigateTo?.child
180
+ return decorated
181
+ }
182
+
183
+ export async function navigateToOnSingleItem() {
184
+ const decorated = await decorateNavigateTo(navigateRows[0])
185
+ void decorated.navigateTo
186
+ return decorated
187
+ }
188
+
189
+ export async function navigateToComposedWithAccess() {
190
+ interface ContentWithNav extends NavigateToDecoratable, AccessDecoratable {
191
+ permission_id?: number[]
192
+ children?: ContentWithNav[]
193
+ }
194
+
195
+ const items: ContentWithNav[] = navigateRows.map((row) => ({
196
+ ...row,
197
+ permission_id: [78],
198
+ }))
199
+
200
+ const withAccess = decorateAccess(items, perms)
201
+ const withBoth = await decorateNavigateTo(withAccess)
202
+
203
+ withBoth[0].need_access satisfies boolean
204
+ void withBoth[0].navigateTo
205
+ return withBoth
206
+ }
207
+
208
+ export async function navigateToParallelWithProgress() {
209
+ interface ContentWithNavAndProgress extends NavigateToDecoratable {
210
+ progress_percent?: number
211
+ }
212
+
213
+ const items = navigateRows as ContentWithNavAndProgress[]
214
+ const decorators: FieldDecoratorAsync<ContentWithNavAndProgress>[] = [
215
+ navigateToDecorator as FieldDecoratorAsync<ContentWithNavAndProgress>,
216
+ {
217
+ field: 'progress_percent',
218
+ compute: (item) => fetchProgress(item.id),
219
+ },
220
+ ]
221
+ const decorated = (await decorateAllAsync(
222
+ items,
223
+ decorators
224
+ )) as WithNavigateTo<ContentWithNavAndProgress>[]
225
+
226
+ void decorated[0].navigateTo
227
+ decorated[0].progress_percent satisfies number | undefined
228
+ return decorated
229
+ }
@@ -0,0 +1,139 @@
1
+ import { decorateAllAsync, type Decoratable, type FieldDecoratorAsync } from './base'
2
+ import {
3
+ findIncompleteLesson,
4
+ getLastInteractedOf,
5
+ getProgressState,
6
+ getProgressStateByIds,
7
+ } from '../../../services/contentProgress.js'
8
+ import {
9
+ COLLECTION_TYPE,
10
+ CollectionParameter,
11
+ STATE,
12
+ } from '../../../services/sync/models/ContentProgress'
13
+
14
+ export const NAVIGATE_TO_FIELD = 'navigateTo' as const
15
+
16
+ const NAVIGABLE_TYPES = [
17
+ 'course',
18
+ 'guided-course',
19
+ 'course-collection',
20
+ 'song-tutorial',
21
+ 'learning-path-v2',
22
+ 'skill-pack',
23
+ ] as const
24
+
25
+ const COURSE_FLOW_TYPES = ['course', 'skill-pack', 'song-tutorial']
26
+ const GUIDED_FLOW_TYPES = ['guided-course', COLLECTION_TYPE.LEARNING_PATH]
27
+ const TWO_DEPTH_TYPES = ['course-collection']
28
+
29
+ export interface NavigateToDecoratable extends Decoratable {
30
+ id: number
31
+ type: string
32
+ brand: string
33
+ thumbnail: string
34
+ published_on: string | null
35
+ status: string
36
+ children?: NavigateToDecoratable[]
37
+ }
38
+
39
+ export interface NavigateTo {
40
+ id: number
41
+ type: string
42
+ brand: string
43
+ thumbnail: string
44
+ published_on: string | null
45
+ status: string
46
+ child: NavigateTo | null
47
+ collection: CollectionParameter | null
48
+ }
49
+
50
+ export type WithNavigateTo<T extends NavigateToDecoratable> = T & {
51
+ navigateTo: NavigateTo | null
52
+ }
53
+
54
+ function buildNavigateTo(
55
+ content: NavigateToDecoratable,
56
+ child: NavigateTo | null = null,
57
+ collection: NavigateTo['collection'] = null
58
+ ): NavigateTo {
59
+ return {
60
+ id: content.id,
61
+ type: content.type,
62
+ brand: content.brand,
63
+ thumbnail: content.thumbnail,
64
+ published_on: content.published_on,
65
+ status: content.status,
66
+ child,
67
+ collection,
68
+ }
69
+ }
70
+
71
+ async function computeNavigateTo(content: NavigateToDecoratable): Promise<NavigateTo | null> {
72
+ if (!NAVIGABLE_TYPES.includes(content.type as (typeof NAVIGABLE_TYPES)[number])) return null
73
+
74
+ const children = content.children
75
+ if (!children || children.length === 0) return null
76
+
77
+ const contentState = await getProgressState(content.id)
78
+ if (contentState !== STATE.STARTED) {
79
+ const firstChild = children[0]
80
+ const childNav = TWO_DEPTH_TYPES.includes(content.type)
81
+ ? await computeNavigateTo(firstChild)
82
+ : null
83
+ return buildNavigateTo(firstChild, childNav)
84
+ }
85
+
86
+ const childrenIds = children.map((c) => c.id)
87
+ const childrenById = new Map(children.map((c) => [c.id, c]))
88
+ const childrenStates = (await getProgressStateByIds(childrenIds)) as Map<number, STATE>
89
+ const lastInteractedId = (await getLastInteractedOf(childrenIds)) as number
90
+
91
+ if (COURSE_FLOW_TYPES.includes(content.type)) {
92
+ const lastInteractedStatus = childrenStates.get(lastInteractedId)
93
+ const targetId =
94
+ lastInteractedStatus === STATE.STARTED
95
+ ? lastInteractedId
96
+ : findIncompleteLesson(childrenStates, lastInteractedId, content.type)
97
+ const target = childrenById.get(targetId)
98
+ return target ? buildNavigateTo(target) : null
99
+ }
100
+
101
+ if (GUIDED_FLOW_TYPES.includes(content.type)) {
102
+ const targetId = findIncompleteLesson(childrenStates, lastInteractedId, content.type)
103
+ const target = childrenById.get(targetId)
104
+ return target ? buildNavigateTo(target) : null
105
+ }
106
+
107
+ if (TWO_DEPTH_TYPES.includes(content.type)) {
108
+ const lastChild = childrenById.get(lastInteractedId)
109
+ if (!lastChild) return null
110
+ const childNav = await computeNavigateTo(lastChild)
111
+ return buildNavigateTo(lastChild, childNav)
112
+ }
113
+
114
+ return null
115
+ }
116
+
117
+ export const navigateToDecorator: FieldDecoratorAsync<
118
+ NavigateToDecoratable,
119
+ typeof NAVIGATE_TO_FIELD,
120
+ NavigateTo | null
121
+ > = {
122
+ field: NAVIGATE_TO_FIELD,
123
+ compute: computeNavigateTo,
124
+ recurse: false,
125
+ }
126
+
127
+ export function decorateNavigateTo<T extends NavigateToDecoratable>(
128
+ items: T[]
129
+ ): Promise<WithNavigateTo<T>[]>
130
+ export function decorateNavigateTo<T extends NavigateToDecoratable>(
131
+ items: T
132
+ ): Promise<WithNavigateTo<T>>
133
+ export function decorateNavigateTo<T extends NavigateToDecoratable>(
134
+ items: T | T[]
135
+ ): Promise<WithNavigateTo<T> | WithNavigateTo<T>[]> {
136
+ return decorateAllAsync(items as NavigateToDecoratable, [navigateToDecorator]) as Promise<
137
+ WithNavigateTo<T> | WithNavigateTo<T>[]
138
+ >
139
+ }
@@ -0,0 +1,40 @@
1
+ import { decorate, type Decoratable, type FieldDecorator } from './base'
2
+ import { getPermissionsAdapter, type UserPermissions } from '../../../services/permissions'
3
+
4
+ export const NEED_ACCESS_FIELD = 'need_access' as const
5
+
6
+ export interface AccessDecoratable extends Decoratable {
7
+ permission_id?: number[]
8
+ children?: AccessDecoratable[]
9
+ }
10
+
11
+ export type WithNeedAccess<T extends AccessDecoratable> = T & {
12
+ need_access: boolean
13
+ children?: WithNeedAccess<NonNullable<T['children']>[number]>[]
14
+ }
15
+
16
+ export function accessDecorator(
17
+ userPermissions: UserPermissions
18
+ ): FieldDecorator<AccessDecoratable, typeof NEED_ACCESS_FIELD, boolean> {
19
+ const adapter = getPermissionsAdapter()
20
+ return {
21
+ field: NEED_ACCESS_FIELD,
22
+ compute: (item) => adapter.doesUserNeedAccess(item, userPermissions),
23
+ }
24
+ }
25
+
26
+ export function decorateAccess<T extends AccessDecoratable>(
27
+ items: T[],
28
+ userPermissions: UserPermissions
29
+ ): WithNeedAccess<T>[]
30
+ export function decorateAccess<T extends AccessDecoratable>(
31
+ items: T,
32
+ userPermissions: UserPermissions
33
+ ): WithNeedAccess<T>
34
+ export function decorateAccess<T extends AccessDecoratable>(
35
+ items: T | T[],
36
+ userPermissions: UserPermissions
37
+ ): WithNeedAccess<T> | WithNeedAccess<T>[] {
38
+ const { field, compute } = accessDecorator(userPermissions)
39
+ return decorate(items as T, field, compute) as WithNeedAccess<T> | WithNeedAccess<T>[]
40
+ }
@@ -0,0 +1,35 @@
1
+ import { decorate, type Decoratable, type FieldDecorator } from './base'
2
+ import { SONG_TYPES_WITH_CHILDREN } from '../../../contentTypeConfig.js'
3
+
4
+ export const PAGE_TYPE_FIELD = 'page_type' as const
5
+
6
+ export type PageType = 'song' | 'lesson'
7
+
8
+ export interface PageTypeDecoratable extends Decoratable {
9
+ type?: string
10
+ children?: PageTypeDecoratable[]
11
+ }
12
+
13
+ export type WithPageType<T extends PageTypeDecoratable> = T & {
14
+ page_type: PageType
15
+ children?: WithPageType<NonNullable<T['children']>[number]>[]
16
+ }
17
+
18
+ export const pageTypeDecorator: FieldDecorator<
19
+ PageTypeDecoratable,
20
+ typeof PAGE_TYPE_FIELD,
21
+ PageType
22
+ > = {
23
+ field: PAGE_TYPE_FIELD,
24
+ compute: (item) => (SONG_TYPES_WITH_CHILDREN.includes(item.type as string) ? 'song' : 'lesson'),
25
+ }
26
+
27
+ export function decoratePageType<T extends PageTypeDecoratable>(items: T[]): WithPageType<T>[]
28
+ export function decoratePageType<T extends PageTypeDecoratable>(items: T): WithPageType<T>
29
+ export function decoratePageType<T extends PageTypeDecoratable>(
30
+ items: T | T[]
31
+ ): WithPageType<T> | WithPageType<T>[] {
32
+ return decorate(items as T, pageTypeDecorator.field, pageTypeDecorator.compute) as
33
+ | WithPageType<T>
34
+ | WithPageType<T>[]
35
+ }
@@ -9,6 +9,7 @@
9
9
  * - `getContentAwardsByIds(contentIds)` - Get awards for multiple content items (batch optimized)
10
10
  * - `getCompletedAwards(brand)` - Get user's earned awards
11
11
  * - `getInProgressAwards(brand)` - Get awards user is working toward
12
+ * - `getCompletedAwardsByUser(userId, brand)` - Get another user's earned awards
12
13
  * - `getAwardStatistics(brand)` - Get aggregate award stats
13
14
  *
14
15
  * **Event Callbacks**:
@@ -58,6 +59,10 @@ import { AwardMessageGenerator } from './internal/message-generator'
58
59
  import db from '../sync/repository-proxy'
59
60
  import UserAwardProgressRepository from '../sync/repositories/user-award-progress'
60
61
  import {awardTemplate} from "../../contentTypeConfig.js";
62
+ import { globalConfig } from '../config.js'
63
+ import { HttpClient } from '../../infrastructure/http/HttpClient'
64
+
65
+ const userManagementBaseUrl = '/api/user-management-system'
61
66
 
62
67
  function enhanceCompletionData(completionData) {
63
68
  if (!completionData) return null
@@ -352,6 +357,72 @@ export async function getCompletedAwards(brand = null, options = {}) {
352
357
  }
353
358
  }
354
359
 
360
+ /**
361
+ * @param {number|null} [userId=globalConfig.sessionConfig.userId] - The user whose completed awards to fetch
362
+ * @param {string|null} [brand=null] - Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands
363
+ * @returns {Promise<AwardInfo[]>} Array of completed award objects sorted by completion date (newest first)
364
+ *
365
+ * @description
366
+ * Returns completed awards for any user (typically used when viewing another
367
+ * user's public profile). Fetches raw progress from the BE then enriches each
368
+ * record with the matching Sanity award definition so the response shape
369
+ * matches `getCompletedAwards`.
370
+ *
371
+ * Returns empty array `[]` on error or when no progress is found.
372
+ */
373
+ export async function getCompletedAwardsByUser(userId = globalConfig.sessionConfig.userId, brand = null) {
374
+ try {
375
+ const apiUrl = `${userManagementBaseUrl}/v1/users/${userId}/awards`
376
+ const httpClient = new HttpClient(globalConfig.baseUrl, globalConfig.sessionConfig.token)
377
+ const progressRecords = await httpClient.get(apiUrl)
378
+
379
+ if (!Array.isArray(progressRecords) || progressRecords.length === 0) {
380
+ return []
381
+ }
382
+
383
+ let awards = await Promise.all(
384
+ progressRecords.map(async (progress) => {
385
+ const definition = await awardDefinitions.getById(progress.award_id)
386
+ if (!definition) {
387
+ return null
388
+ }
389
+
390
+ if (brand && definition.brand !== brand) {
391
+ return null
392
+ }
393
+
394
+ const completionData = definition.type === awardDefinitions.CONTENT_AWARD
395
+ ? enhanceCompletionData(progress.completion_data)
396
+ : progress.completion_data
397
+ const hasCertificate = definition.type === awardDefinitions.CONTENT_AWARD
398
+
399
+ return {
400
+ awardId: progress.award_id,
401
+ awardTitle: definition.name,
402
+ awardType: definition.type,
403
+ ...getBadgeFields(definition),
404
+ award: definition.award,
405
+ brand: definition.brand,
406
+ hasCertificate,
407
+ instructorName: definition.instructor_name,
408
+ progressPercentage: progress.progress_percentage,
409
+ isCompleted: true,
410
+ completedAt: progress.completed_at,
411
+ completionData,
412
+ }
413
+ })
414
+ )
415
+
416
+ awards = awards.filter(award => award !== null)
417
+ awards.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime())
418
+
419
+ return awards
420
+ } catch (error) {
421
+ console.error(`Failed to get completed awards for user ${userId}:`, error)
422
+ return []
423
+ }
424
+ }
425
+
355
426
  /**
356
427
  * @param {string} [brand=null] - Brand to filter by (drumeo, pianote, guitareo, singeo), or null for all brands
357
428
  * @param {AwardPaginationOptions} [options={}] - Optional pagination options
@@ -317,7 +317,7 @@ function addRecordIdsToData(data, dataField, isDataAnArray, includeParent, inclu
317
317
  items.push(content)
318
318
  recordIds.push(content.record_id)
319
319
  }
320
- if (includeIntroVideo) {
320
+ if (includeIntroVideo && content.intro_video?.id) {
321
321
  content.intro_video.record_id = generateRecordId(content.intro_video.id, null)
322
322
  items.push(content.intro_video)
323
323
  recordIds.push(content.intro_video.record_id)
@@ -21,12 +21,14 @@ export interface InviteResponse {
21
21
  is_account_valid: boolean
22
22
  is_invite_active: boolean
23
23
  can_user_join: boolean
24
+ primary_user_name: string
25
+ product_name: string
24
26
  // These fields leak user information and are excluded entirely for the public endpoint
25
27
  existing_user_details?: User
26
28
  email?: string
27
29
  }
28
30
 
29
- export interface UsersMultiAccountResponse {
31
+ export interface UsersDataForMultiUserAccount {
30
32
  user_id: number
31
33
  active_multi_user_account: MultiUserAccountResponse
32
34
  last_cancelled_multi_user_account: MultiUserAccountResponse
@@ -40,13 +42,15 @@ export interface MultiUserAccountResponse {
40
42
  product_name: string
41
43
  is_active: boolean
42
44
  primary_user: User
43
- total_seats: number
44
45
  end_time: string
45
46
  is_primary_account_holder: boolean
47
+ membership_level: 'plus' | 'basic'
48
+ is_lifetime_addon: boolean
46
49
  // The following fields are not included for public or subaccount users
47
- active_invited_emails?: InviteResponse[]
50
+ active_invites?: InviteResponse[]
48
51
  available_seats?: number
49
- available_invites?: InviteResponse[]
52
+ available_invites?: number
53
+ total_seats?: number
50
54
  active_subs?: User[]
51
55
  show_welcome?: boolean
52
56
  }
@@ -81,12 +85,12 @@ export async function createAccount(params: CreateAccountParams): Promise<MultiU
81
85
  * Fetches multi-user account details for a specific user.
82
86
  *
83
87
  * @param {number} userId - The ID of the user to fetch account details for.
84
- * @returns {Promise<UsersMultiAccountResponse>} - A promise that resolves to the account details.
88
+ * @returns {Promise<UsersDataForMultiUserAccount>} - A promise that resolves to the account details.
85
89
  * @throws {HttpError} - If the HTTP request fails.
86
90
  */
87
- export async function fetchUsersMultiAccountDetails(userId: number): Promise<UsersMultiAccountResponse> {
91
+ export async function fetchUsersMultiAccountDetails(userId: number): Promise<UsersDataForMultiUserAccount> {
88
92
  const httpClient = new HttpClient(globalConfig.baseUrl)
89
- return httpClient.get<UsersMultiAccountResponse>(`${baseUrl}/${userId}/details`)
93
+ return httpClient.get<UsersDataForMultiUserAccount>(`${baseUrl}/${userId}/details`)
90
94
  }
91
95
 
92
96