@@ -4,6 +4,7 @@ import { SERVER_ERROR } from 'upgrade_types';
44import { AppRequest } from '../../types' ;
55import { Service } from 'typedi' ;
66import { ExperimentUserService } from '../services/ExperimentUserService' ;
7+ import { RequestedExperimentUser } from '../controllers/validators/ExperimentUserValidator' ;
78
89@Service ( )
910export class UserCheckMiddleware {
@@ -22,14 +23,23 @@ export class UserCheckMiddleware {
2223 req . logger . child ( { user_id } ) ;
2324 req . logger . debug ( { message : 'User Id is:' , user_id } ) ;
2425 }
25- const experimentUserDoc = await this . experimentUserService . getUserDoc ( user_id , req . logger ) ;
26+
27+ let experimentUserDoc : RequestedExperimentUser ;
28+
29+ if ( req . url . endsWith ( '/v6/featureflag' ) ) {
30+ experimentUserDoc = await this . handleProvidedGroupsForSession ( req , user_id ) ;
31+ } else {
32+ experimentUserDoc = await this . experimentUserService . getUserDoc ( user_id , req . logger ) ;
33+ }
34+
2635 if ( ! req . url . endsWith ( '/init' ) && ! experimentUserDoc ) {
2736 const error = new Error ( `User not found: ${ user_id } ` ) ;
2837 ( error as any ) . type = SERVER_ERROR . EXPERIMENT_USER_NOT_DEFINED ;
2938 ( error as any ) . httpCode = 404 ;
3039 req . logger . error ( error ) ;
3140 return next ( error ) ;
3241 }
42+
3343 req . userDoc = experimentUserDoc ;
3444 // Continue to the next middleware/controller
3545 return next ( ) ;
@@ -38,4 +48,154 @@ export class UserCheckMiddleware {
3848 return next ( error ) ;
3949 }
4050 }
51+
52+ /**
53+ * Handles potential ephemeral session groups provided in the request.
54+ *
55+ * This method processes different scenarios based on the combination of
56+ * `groupsForSession` and `includeStoredUserGroups` parameters:
57+ *
58+ * Note: provided session groups are never persisted
59+ *
60+ * **Stored-user Mode** (Standard stored user lookup):
61+ * - Omit both `groupsForSession` and `includeStoredUserGroups` parameters
62+ * - Uses only stored user groups from the database
63+ * - User must already have been initialized, will 404 if user does not exist
64+ *
65+ * **Ephemeral Mode** (Session-only groups):
66+ * - Set `includeStoredUserGroups` to `false` and provide `groupsForSession`
67+ * - Uses only the groups provided in the session, ignoring any stored user groups.
68+ * - Does not require the user to be initialized (it will bypass stored user lookup)
69+ * - Useful when complete group information is always provided at runtime.
70+ *
71+ * **Merged Mode** (Stored + Session groups):
72+ * - Set `includeStoredUserGroups` to `true` and provide `groupsForSession`
73+ * - User must already have been initialized, will 404 if user does not exist.
74+ * - Session groups are merged with stored groups if they don't already exist for stored user.
75+ * - Session groups are never persisted.
76+ * - Useful for adding context-specific ephemeral groups to an existing user.
77+ *
78+ * @param req - The application request containing session group parameters
79+ * @param user_id - The user identifier for group lookup
80+ * @returns Promise resolving to RequestedExperimentUser with appropriate group configuration
81+ *
82+ * @example
83+ * ```typescript
84+ * // Scenario 1: Standard lookup (no session params)
85+ * {
86+ * context: "storedUserDependentContext",
87+ * }
88+ *
89+ * // Scenario 2: Ignore stored user and use session groups only
90+ * {
91+ * context: "independentContext",
92+ * groupsForSession: { "classId": ["testClass"], "schoolId": ["instructor"] },
93+ * includeStoredUserGroups: false
94+ * }
95+ *
96+ * // Scenario 3: Merge session and stored groups
97+ * {
98+ * context: "storedUserMergedContext",
99+ * groupsForSession: { "classId": ["testClass"], "schoolId": ["demoSchool"] },
100+ * includeStoredUserGroups: true
101+ * }
102+ *
103+ * ```
104+ */
105+ private async handleProvidedGroupsForSession ( req : AppRequest , user_id : string ) : Promise < RequestedExperimentUser > {
106+ // Note: validation of request will have occurred before this middleware
107+
108+ // Scenario 1: Session-only groups (ephemeral user mode)
109+ // explicitly check if includeStoredUserGroups is exactly false and not just undefined
110+ if ( req . body . groupsForSession && req . body . includeStoredUserGroups === false ) {
111+ const experimentUserDoc = this . createSessionUser ( user_id , req . body . groupsForSession ) ;
112+
113+ req . logger . debug ( {
114+ message : 'Created ephemeral user with session groups' ,
115+ experimentUserDoc,
116+ } ) ;
117+
118+ return experimentUserDoc ;
119+ }
120+
121+ // Load stored user document, required for scenarios 2 and 3
122+ const experimentUserDoc = await this . experimentUserService . getUserDoc ( user_id , req . logger ) ;
123+
124+ if ( ! experimentUserDoc ) {
125+ return null ; // User not found, will be handled in the main middleware
126+ }
127+
128+ if ( req . body . groupsForSession && req . body . includeStoredUserGroups ) {
129+ // Scenario 2: Merged groups (Merged stored/ephemeral groups mode)
130+ experimentUserDoc . group = this . mergeGroupsWithUniqueValues ( experimentUserDoc . group , req . body . groupsForSession ) ;
131+
132+ req . logger . debug ( {
133+ message : 'Merged session groups with stored user groups' ,
134+ experimentUserDoc,
135+ } ) ;
136+ } else {
137+ // Scenario 3: Standard behavior (user-lookup from user-id)
138+ req . logger . debug ( {
139+ message : 'Using standard user lookup without session group modifications' ,
140+ experimentUserDoc,
141+ } ) ;
142+ }
143+
144+ return experimentUserDoc ;
145+ }
146+
147+ /**
148+ * Creates a RequestedExperimentUser object for ephemeral user scenarios.
149+ *
150+ * @param user_id - The user ID for the experiment user
151+ * @param groupsForSession - The groups provided in the session
152+ * @returns RequestedExperimentUser object with session groups
153+ */
154+ private createSessionUser ( user_id : string , groupsForSession : Record < string , string [ ] > ) : RequestedExperimentUser {
155+ const sessionUser = new RequestedExperimentUser ( ) ;
156+ sessionUser . id = user_id ;
157+ sessionUser . requestedUserId = user_id ;
158+ sessionUser . group = groupsForSession ;
159+ sessionUser . workingGroup = undefined ;
160+ return sessionUser ;
161+ }
162+
163+ /**
164+ * Merges two group objects with unique values per key.
165+ *
166+ * This utility method combines existing user groups with incoming session groups,
167+ * ensuring no duplicate values exist within each group key's array.
168+ *
169+ * @param existing - The existing user groups (may be undefined)
170+ * @param incoming - The incoming session groups to merge
171+ * @returns A new object with merged groups containing unique values
172+ *
173+ * @example
174+ * ```typescript
175+ * const existing = { "classId": ["123", "xyz"], "schoolId": ["qwerty"] };
176+ * const incoming = { "classId": ["abc", "123"], "instructorId": ["dale123"] };
177+ * const merged = mergeGroupsWithUniqueValues(existing, incoming);
178+ * // Result: {
179+ * // "classId": ["123", "xyz", "abc"],
180+ * // "schoolId": ["qwerty"],
181+ * // "instructorId": ["dale123"]
182+ * // }
183+ * ```
184+ */
185+ private mergeGroupsWithUniqueValues (
186+ existing : Record < string , string [ ] > | undefined ,
187+ incoming : Record < string , string [ ] >
188+ ) : Record < string , string [ ] > {
189+ const result : Record < string , string [ ] > = { ...existing } ;
190+
191+ for ( const [ key , values ] of Object . entries ( incoming ) ) {
192+ if ( result [ key ] ) {
193+ result [ key ] = [ ...new Set ( [ ...result [ key ] , ...values ] ) ] ;
194+ } else {
195+ result [ key ] = [ ...values ] ;
196+ }
197+ }
198+
199+ return result ;
200+ }
41201}
0 commit comments