Skip to content

Commit 0aac871

Browse files
committed
Parsers now return copied versions of the original corrected attributes.
This makes sure we cannot introduce data that is unreadable in the survey review questionnaire. Also remove non necessary logs from survey objects factories. Also update the tests to fix the remaining expect.any(SurveyObjectsRegistry) that should have used the survey objects registry directly.
1 parent 378aa10 commit 0aac871

File tree

9 files changed

+82
-77
lines changed

9 files changed

+82
-77
lines changed

packages/evolution-backend/src/services/audits/__tests__/SurveyObjectParsers.test.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('SurveyObjectParsers Integration', () => {
4444

4545
describe('Parser Configuration Integration', () => {
4646
it('should call interview parser when configured in SurveyObjectsFactory', async () => {
47-
const mockInterviewParser = jest.fn();
47+
const mockInterviewParser = jest.fn().mockReturnValue({ _language: 'en' });
4848

4949
setProjectConfig({
5050
surveyObjectParsers: {
@@ -67,7 +67,10 @@ describe('SurveyObjectParsers Integration', () => {
6767
});
6868

6969
it('should call home parser when configured in SurveyObjectsFactory', async () => {
70-
const mockHomeParser = jest.fn();
70+
const mockHomeParser = jest.fn().mockReturnValue({
71+
_uuid: uuidV4(),
72+
address: '123 Test St'
73+
});
7174

7275
setProjectConfig({
7376
surveyObjectParsers: {
@@ -96,7 +99,10 @@ describe('SurveyObjectParsers Integration', () => {
9699
});
97100

98101
it('should call household parser when configured in SurveyObjectsFactory', async () => {
99-
const mockHouseholdParser = jest.fn();
102+
const mockHouseholdParser = jest.fn().mockReturnValue({
103+
_uuid: uuidV4(),
104+
size: 2
105+
});
100106

101107
setProjectConfig({
102108
surveyObjectParsers: {
@@ -125,15 +131,19 @@ describe('SurveyObjectParsers Integration', () => {
125131
});
126132

127133
it('should call person parser when configured in PersonFactory', async () => {
128-
const mockPersonParser = jest.fn();
134+
const mockPersonParser = jest.fn().mockReturnValue({
135+
_uuid: 'person-uuid',
136+
_sequence: 1,
137+
age: 30
138+
});
129139

130140
setProjectConfig({
131141
surveyObjectParsers: {
132142
person: mockPersonParser
133143
}
134144
});
135145

136-
const personUuid = uuidV4();
146+
const personUuid = 'person-uuid';
137147
const householdUuid = uuidV4();
138148

139149
// Create a household object first
@@ -188,7 +198,10 @@ describe('SurveyObjectParsers Integration', () => {
188198
});
189199

190200
it('should call journey parser when configured in JourneyFactory', async () => {
191-
const mockJourneyParser = jest.fn();
201+
const mockJourneyParser = jest.fn().mockReturnValue({
202+
_uuid: 'journey-uuid',
203+
_sequence: 1
204+
});
192205

193206
setProjectConfig({
194207
surveyObjectParsers: {
@@ -248,7 +261,10 @@ describe('SurveyObjectParsers Integration', () => {
248261
});
249262

250263
it('should call trip parser when configured in TripFactory', async () => {
251-
const mockTripParser = jest.fn();
264+
const mockTripParser = jest.fn().mockReturnValue({
265+
_uuid: 'trip-uuid',
266+
_sequence: 1
267+
});
252268

253269
setProjectConfig({
254270
surveyObjectParsers: {
@@ -310,7 +326,11 @@ describe('SurveyObjectParsers Integration', () => {
310326
});
311327

312328
it('should call segment parser when configured in SegmentFactory', async () => {
313-
const mockSegmentParser = jest.fn();
329+
const mockSegmentParser = jest.fn().mockReturnValue({
330+
_uuid: 'segment-uuid',
331+
_sequence: 1,
332+
mode: 'walk'
333+
});
314334

315335
setProjectConfig({
316336
surveyObjectParsers: {
@@ -371,7 +391,11 @@ describe('SurveyObjectParsers Integration', () => {
371391
});
372392

373393
it('should call visitedPlace parser when configured in VisitedPlaceFactory', async () => {
374-
const mockVisitedPlaceParser = jest.fn();
394+
const mockVisitedPlaceParser = jest.fn().mockReturnValue({
395+
_uuid: 'place-uuid',
396+
_sequence: 1,
397+
activity: 'home'
398+
});
375399

376400
setProjectConfig({
377401
surveyObjectParsers: {

packages/evolution-backend/src/services/audits/types.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ import { CorrectedResponse } from 'evolution-common/lib/services/questionnaire/t
1717
/**
1818
* Parser function type for converting survey response values to proper types
1919
* before object validation. For example, converting 'yes'/'no' strings to boolean values.
20-
* Parsers modify the corrected response in place.
20+
*
21+
* All parser functions must return a copied/cloned version of the corrected attributes (TInput)
2122
*/
22-
export type SurveyObjectParserInterview<TInput> = (attributes: TInput) => void;
23+
export type SurveyObjectParserInterview<CorrectedResponse> = (
24+
originalCorrectedResponse: Readonly<CorrectedResponse>
25+
) => CorrectedResponse;
2326
export type SurveyObjectParser<TInput, CorrectedResponse> = (
24-
attributes: TInput,
25-
correctedResponse: CorrectedResponse
26-
) => void;
27+
originalCorrectedAttributes: Readonly<TInput>,
28+
correctedResponse: Readonly<CorrectedResponse>
29+
) => TInput;
2730

2831
/**
2932
* Configuration for survey object parsers.

packages/evolution-backend/src/services/surveyObjects/JourneyFactory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,14 @@ export async function populateJourneysForPerson(
4848
return sequenceA - sequenceB;
4949
});
5050

51-
for (const [journeyUuid, journeyAttributes] of sortedJourneyEntries) {
51+
for (const [journeyUuid, originalCorrectedJourneyAttributes] of sortedJourneyEntries) {
5252
if (journeyUuid === 'undefined') {
5353
continue;
5454
}
5555

56-
// Parse journey attributes if parser is available
57-
if (projectConfig.surveyObjectParsers?.journey) {
58-
projectConfig.surveyObjectParsers.journey(journeyAttributes, correctedResponse);
59-
}
56+
const journeyAttributes = projectConfig.surveyObjectParsers?.journey
57+
? projectConfig.surveyObjectParsers.journey(originalCorrectedJourneyAttributes, correctedResponse)
58+
: originalCorrectedJourneyAttributes;
6059

6160
const journey = Journey.create(
6261
_omit(journeyAttributes as { [key: string]: unknown }, [

packages/evolution-backend/src/services/surveyObjects/PersonFactory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,14 @@ export async function populatePersonsForHousehold(
5050
// Track person index for color assignment
5151
let personIndex = 0;
5252

53-
for (const [personUuid, personAttributes] of sortedPersonEntries) {
53+
for (const [personUuid, originalCorrectedPersonAttributes] of sortedPersonEntries) {
5454
if (personUuid === 'undefined') {
5555
continue; // ignore if uuid is undefined
5656
}
5757

58-
// Parse person attributes if parser is available
59-
if (projectConfig.surveyObjectParsers?.person) {
60-
projectConfig.surveyObjectParsers.person(personAttributes, correctedResponse);
61-
}
58+
const personAttributes = projectConfig.surveyObjectParsers?.person
59+
? projectConfig.surveyObjectParsers.person(originalCorrectedPersonAttributes, correctedResponse)
60+
: originalCorrectedPersonAttributes;
6261

6362
const personResult = Person.create(personAttributes as { [key: string]: unknown }, surveyObjectsRegistry);
6463

packages/evolution-backend/src/services/surveyObjects/SegmentFactory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,14 @@ export async function populateSegmentsForTrip(
3939
return sequenceA - sequenceB;
4040
});
4141

42-
for (const [segmentUuid, segmentAttributes] of sortedSegmentEntries) {
42+
for (const [segmentUuid, originalCorrectedSegmentAttributes] of sortedSegmentEntries) {
4343
if (segmentUuid === 'undefined') {
4444
continue;
4545
}
4646

47-
// Parse segment attributes if parser is available
48-
if (projectConfig.surveyObjectParsers?.segment) {
49-
projectConfig.surveyObjectParsers.segment(segmentAttributes, correctedResponse);
50-
}
47+
const segmentAttributes = projectConfig.surveyObjectParsers?.segment
48+
? projectConfig.surveyObjectParsers.segment(originalCorrectedSegmentAttributes, correctedResponse)
49+
: originalCorrectedSegmentAttributes;
5150

5251
const segment = Segment.create(segmentAttributes as ExtendedSegmentAttributes, surveyObjectsRegistry);
5352

packages/evolution-backend/src/services/surveyObjects/SurveyObjectsFactory.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import { populateJourneysForPerson } from './JourneyFactory';
1717
import { ExtendedPersonAttributes } from 'evolution-common/lib/services/baseObjects/Person';
1818
import projectConfig from '../../config/projectConfig';
1919
import { Home } from 'evolution-common/lib/services/baseObjects/Home';
20-
import { Household } from 'evolution-common/lib/services/baseObjects/Household';
20+
import { ExtendedHouseholdAttributes, Household } from 'evolution-common/lib/services/baseObjects/Household';
2121
import { SurveyObjectsRegistry } from 'evolution-common/lib/services/baseObjects/SurveyObjectsRegistry';
22+
import { ExtendedPlaceAttributes } from 'evolution-common/lib/services/baseObjects/Place';
2223

2324
/**
2425
* Configuration options for object creation
@@ -84,9 +85,7 @@ export class SurveyObjectsFactory {
8485
}
8586
};
8687

87-
const correctedResponse = interviewAttributes.corrected_response;
88-
89-
if (!correctedResponse) {
88+
if (!interviewAttributes.corrected_response) {
9089
surveyObjectsWithErrors.errorsByObject.interview = [
9190
new Error(
9291
'Interview validateParams: Corrected response is missing from interview attributes. It must be generated from original responses on first review.'
@@ -99,12 +98,9 @@ export class SurveyObjectsFactory {
9998
const surveyObjectsRegistry = new SurveyObjectsRegistry();
10099

101100
// Create Interview
102-
console.log('==== Interview creation ====');
103-
104-
// Parse interview attributes if parser is available
105-
if (projectConfig.surveyObjectParsers?.interview) {
106-
projectConfig.surveyObjectParsers.interview(correctedResponse);
107-
}
101+
const correctedResponse = projectConfig.surveyObjectParsers?.interview
102+
? projectConfig.surveyObjectParsers.interview(interviewAttributes.corrected_response)
103+
: interviewAttributes.corrected_response;
108104

109105
const interviewResult = createInterviewObject(
110106
_omit(correctedResponse, ['home', 'household']) as CorrectedResponse,
@@ -113,29 +109,25 @@ export class SurveyObjectsFactory {
113109
);
114110
if (isOk(interviewResult)) {
115111
surveyObjectsWithErrors.interview = interviewResult.result;
116-
console.log('==== Interview created successfully ====');
117112
} else {
118113
surveyObjectsWithErrors.errorsByObject.interview = interviewResult.errors;
119114
console.log(
120115
`==== Interview creation failed with errors count: ${interviewResult.errors?.length || 0} ====`
121116
);
122117
}
123118

124-
console.log(' ==== Home creation ====');
125-
126-
// Parse interview attributes if parser is available
127-
if (projectConfig.surveyObjectParsers?.home && correctedResponse.home) {
128-
projectConfig.surveyObjectParsers.home(correctedResponse.home, correctedResponse);
129-
}
130-
131-
const homeAttributes = correctedResponse.home;
119+
const homeAttributes = projectConfig.surveyObjectParsers?.home
120+
? projectConfig.surveyObjectParsers.home(
121+
correctedResponse.home as ExtendedPlaceAttributes,
122+
correctedResponse
123+
)
124+
: correctedResponse.home;
132125

133126
// Only try to create home if we have home attributes
134127
if (homeAttributes) {
135128
const homeResult = Home.create(homeAttributes as { [key: string]: unknown }, surveyObjectsRegistry);
136129
if (isOk(homeResult)) {
137130
surveyObjectsWithErrors.home = homeResult.result;
138-
console.log(' ==== Home created successfully ====');
139131
} else {
140132
surveyObjectsWithErrors.errorsByObject.home = homeResult.errors;
141133
console.log(` ==== Home creation failed with errors count: ${homeResult.errors?.length || 0} ====`);
@@ -144,14 +136,12 @@ export class SurveyObjectsFactory {
144136
console.log(' ==== Home creation skipped (no home attributes) ====');
145137
}
146138

147-
console.log(' ==== Household creation ====');
148-
149-
// Parse interview attributes if parser is available
150-
if (projectConfig.surveyObjectParsers?.household && correctedResponse.household) {
151-
projectConfig.surveyObjectParsers.household(correctedResponse.household, correctedResponse);
152-
}
153-
154-
const householdAttributes = correctedResponse.household;
139+
const householdAttributes = projectConfig.surveyObjectParsers?.household
140+
? projectConfig.surveyObjectParsers.household(
141+
correctedResponse.household as ExtendedHouseholdAttributes,
142+
correctedResponse
143+
)
144+
: correctedResponse.household;
155145

156146
// Only try to create household if we have household attributes
157147
if (householdAttributes) {
@@ -161,7 +151,6 @@ export class SurveyObjectsFactory {
161151
);
162152
if (isOk(householdResult)) {
163153
surveyObjectsWithErrors.household = householdResult.result;
164-
console.log(' ==== Household created successfully ====');
165154
} else {
166155
surveyObjectsWithErrors.errorsByObject.household = householdResult.errors;
167156
console.log(
@@ -177,9 +166,6 @@ export class SurveyObjectsFactory {
177166

178167
// Continue with persons, journeys, etc. if household and home were created
179168
if (household && householdAttributes) {
180-
// Log the creation of persons
181-
console.log(' ==== Persons creation ====');
182-
183169
// For now, we'll keep the existing factory functions for persons/journeys
184170
// These can be refactored later in the same way
185171
await populatePersonsForHousehold(
@@ -198,9 +184,6 @@ export class SurveyObjectsFactory {
198184
const personUuid = person._uuid!;
199185
const personAttributes = personsAttributes[personUuid] as ExtendedPersonAttributes;
200186

201-
// Log the creation of journeys, visited places, trips and segments
202-
console.log(' ==== Journeys, visited places, trips and segments creation ====');
203-
204187
// Generate all journeys for this person (includes visited places, trips, and segments)
205188
await populateJourneysForPerson(
206189
surveyObjectsWithErrors,

packages/evolution-backend/src/services/surveyObjects/TripFactory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,14 @@ export async function populateTripsForJourney(
4545
return sequenceA - sequenceB;
4646
});
4747

48-
for (const [tripUuid, tripAttributes] of sortedTripEntries) {
48+
for (const [tripUuid, originalCorrectedTripAttributes] of sortedTripEntries) {
4949
if (tripUuid === 'undefined') {
5050
continue;
5151
}
5252

53-
// Parse trip attributes if parser is available
54-
if (projectConfig.surveyObjectParsers?.trip) {
55-
projectConfig.surveyObjectParsers.trip(tripAttributes, correctedResponse);
56-
}
53+
const tripAttributes = projectConfig.surveyObjectParsers?.trip
54+
? projectConfig.surveyObjectParsers.trip(originalCorrectedTripAttributes, correctedResponse)
55+
: originalCorrectedTripAttributes;
5756

5857
const trip = Trip.create(
5958
_omit(tripAttributes as { [key: string]: unknown }, ['segments']) as ExtendedTripAttributes,

packages/evolution-backend/src/services/surveyObjects/VisitedPlaceFactory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,14 @@ export async function populateVisitedPlacesForJourney(
4646
return sequenceA - sequenceB;
4747
});
4848

49-
for (const [visitedPlaceUuid, visitedPlaceAttributes] of sortedVisitedPlaceEntries) {
49+
for (const [visitedPlaceUuid, originalCorrectedVisitedPlaceAttributes] of sortedVisitedPlaceEntries) {
5050
if (visitedPlaceUuid === 'undefined') {
5151
continue;
5252
}
5353

54-
// Parse visited place attributes if parser is available
55-
if (projectConfig.surveyObjectParsers?.visitedPlace) {
56-
projectConfig.surveyObjectParsers.visitedPlace(visitedPlaceAttributes, correctedResponse);
57-
}
54+
const visitedPlaceAttributes = projectConfig.surveyObjectParsers?.visitedPlace
55+
? projectConfig.surveyObjectParsers.visitedPlace(originalCorrectedVisitedPlaceAttributes, correctedResponse)
56+
: originalCorrectedVisitedPlaceAttributes;
5857

5958
const visitedPlaceResult = VisitedPlace.create(
6059
visitedPlaceAttributes as ExtendedVisitedPlaceAttributes,

packages/evolution-backend/src/services/surveyObjects/__tests__/TripFactory.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('TripFactory', () => {
138138
_originVisitedPlaceUuid: 'origin-vp-uuid',
139139
_destinationVisitedPlaceUuid: 'destination-vp-uuid'
140140
}),
141-
expect.any(SurveyObjectsRegistry)
141+
surveyObjectsRegistry
142142
);
143143
expect(MockedTrip.create).toHaveBeenCalledWith(
144144
expect.objectContaining({
@@ -147,7 +147,7 @@ describe('TripFactory', () => {
147147
_originVisitedPlaceUuid: 'destination-vp-uuid',
148148
_destinationVisitedPlaceUuid: 'origin-vp-uuid'
149149
}),
150-
expect.any(SurveyObjectsRegistry)
150+
surveyObjectsRegistry
151151
);
152152

153153
// Verify trips were added to journey
@@ -359,7 +359,7 @@ describe('TripFactory', () => {
359359
mockTrip,
360360
journeyAttributes.trips!['trip-1'],
361361
{ uuid: 'test' },
362-
expect.any(SurveyObjectsRegistry)
362+
surveyObjectsRegistry
363363
);
364364
});
365365
});

0 commit comments

Comments
 (0)