Skip to content

Commit 52e6bf3

Browse files
authored
feat(api): return all errors on import csv
1 parent 0c06f41 commit 52e6bf3

File tree

14 files changed

+435
-296
lines changed

14 files changed

+435
-296
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { HttpErrors } from '../../../shared/application/http-errors.js';
2+
import { AggregateImportError } from '../domain/errors.js';
3+
4+
const learnerManagementDomainErrorMappingConfiguration = [
5+
{
6+
name: AggregateImportError.name,
7+
httpErrorFn: (error) => {
8+
return new HttpErrors.PreconditionFailedError(error.message, error.code, error.meta);
9+
},
10+
},
11+
];
12+
13+
export { learnerManagementDomainErrorMappingConfiguration };

api/src/prescription/learner-management/domain/errors.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,16 @@ class SiecleXmlImportError extends DomainError {
2020
}
2121
}
2222

23-
export { OrganizationDoesNotHaveFeatureEnabledError, OrganizationLearnersCouldNotBeSavedError, SiecleXmlImportError };
23+
class AggregateImportError extends DomainError {
24+
constructor(meta) {
25+
super('An error occurred during import validation');
26+
this.meta = meta;
27+
}
28+
}
29+
30+
export {
31+
AggregateImportError,
32+
OrganizationDoesNotHaveFeatureEnabledError,
33+
OrganizationLearnersCouldNotBeSavedError,
34+
SiecleXmlImportError,
35+
};

api/src/prescription/learner-management/domain/validators/organization-learner-validator.js

+44-37
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EntityValidationError } from '../../../../shared/domain/errors.js';
55
import { OrganizationLearner } from '../models/OrganizationLearner.js';
66

77
const { STUDENT, APPRENTICE } = OrganizationLearner.STATUS;
8-
const validationConfiguration = { allowUnknown: true };
8+
const validationConfiguration = { allowUnknown: true, abortEarly: false };
99
const MAX_LENGTH = 255;
1010
const CITY_CODE_LENGTH = 5;
1111
const PROVINCE_CODE_MIN_LENGTH = 2;
@@ -46,46 +46,53 @@ const validationSchema = Joi.object({
4646
});
4747

4848
const checkValidation = function (organizationLearner) {
49+
const errors = [];
4950
const { error } = validationSchema.validate(organizationLearner, validationConfiguration);
5051

5152
if (error) {
52-
const err = EntityValidationError.fromJoiErrors(error.details);
53-
const { type, context } = error.details[0];
54-
if (type === 'any.required') {
55-
err.why = 'required';
56-
}
57-
if (type === 'string.max') {
58-
err.why = 'max_length';
59-
err.limit = context.limit;
60-
}
61-
if (type === 'string.length') {
62-
err.why = 'length';
63-
err.limit = context.limit;
64-
}
65-
if (type === 'string.min') {
66-
err.why = 'min_length';
67-
err.limit = context.limit;
68-
}
69-
if (type === 'string.pattern.base' && ['birthCountryCode', 'birthCityCode'].includes(context.key)) {
70-
err.why = 'not_valid_insee_code';
71-
}
72-
if (type === 'date.format') {
73-
err.why = 'not_a_date';
74-
}
75-
if (type === 'date.base') {
76-
err.why = 'not_a_date';
77-
}
78-
if (type === 'any.only') {
79-
err.why = 'bad_values';
80-
err.valids = context.valids;
81-
}
82-
if (type === 'string.pattern.name') {
83-
err.why = 'bad_pattern';
84-
err.pattern = context.name;
85-
}
86-
err.key = context.key;
87-
throw err;
53+
error.details.forEach((error) => {
54+
const err = EntityValidationError.fromJoiError(error);
55+
56+
const { type, context } = error;
57+
if (type === 'any.required') {
58+
err.why = 'required';
59+
}
60+
if (type === 'string.max') {
61+
err.why = 'max_length';
62+
err.limit = context.limit;
63+
}
64+
if (type === 'string.length') {
65+
err.why = 'length';
66+
err.limit = context.limit;
67+
}
68+
if (type === 'string.min') {
69+
err.why = 'min_length';
70+
err.limit = context.limit;
71+
}
72+
if (type === 'string.pattern.base' && ['birthCountryCode', 'birthCityCode'].includes(context.key)) {
73+
err.why = 'not_valid_insee_code';
74+
}
75+
if (type === 'date.format') {
76+
err.why = 'not_a_date';
77+
}
78+
if (type === 'date.base') {
79+
err.why = 'not_a_date';
80+
}
81+
if (type === 'any.only') {
82+
err.why = 'bad_values';
83+
err.valids = context.valids;
84+
}
85+
if (type === 'string.pattern.name') {
86+
err.why = 'bad_pattern';
87+
err.pattern = context.name;
88+
}
89+
err.key = context.key;
90+
91+
errors.push(err);
92+
});
8893
}
94+
95+
return errors;
8996
};
9097

9198
export { checkValidation, FRANCE_COUNTRY_CODE };

api/src/prescription/learner-management/infrastructure/serializers/csv/csv-learner-parser.js api/src/prescription/learner-management/infrastructure/serializers/csv/csv-organization-learner-parser.js

+73-49
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import papa from 'papaparse';
33

44
import { CsvImportError } from '../../../../../shared/domain/errors.js';
55
import { convertDateValue } from '../../../../../shared/infrastructure/utils/date-utils.js';
6+
import { AggregateImportError } from '../../../domain/errors.js';
67

78
const ERRORS = {
89
ENCODING_NOT_SUPPORTED: 'ENCODING_NOT_SUPPORTED',
@@ -34,29 +35,52 @@ class CsvOrganizationLearnerParser {
3435
constructor(input, organizationId, columns, learnerSet) {
3536
this._input = input;
3637
this._organizationId = organizationId;
38+
this._errors = [];
3739
this._columns = columns;
3840
this.learnerSet = learnerSet;
41+
this._supportedErrors = [
42+
'min_length',
43+
'max_length',
44+
'length',
45+
'date_format',
46+
'email_format',
47+
'required',
48+
'bad_values',
49+
];
3950
}
4051

4152
parse(encoding) {
42-
const { learnerLines, fields } = this._parse(encoding);
43-
4453
if (!encoding) {
45-
throw new CsvImportError(ERRORS.ENCODING_NOT_SUPPORTED);
54+
this._errors.push(new CsvImportError(ERRORS.ENCODING_NOT_SUPPORTED));
4655
}
4756

57+
this.throwHasErrors();
58+
59+
const { learnerLines, fields } = this._parse(encoding);
60+
61+
this.throwHasErrors();
62+
4863
this._checkColumns(fields);
64+
65+
this.throwHasErrors();
66+
4967
learnerLines.forEach((line, index) => {
5068
const learnerAttributes = this._lineToOrganizationLearnerAttributes(line);
5169
try {
5270
this.learnerSet.addLearner(learnerAttributes);
53-
} catch (err) {
54-
this._handleError(err, index);
71+
} catch (errors) {
72+
this._handleValidationError(errors, index);
5573
}
5674
});
75+
76+
this.throwHasErrors();
5777
return this.learnerSet;
5878
}
5979

80+
throwHasErrors() {
81+
if (this._errors.length > 0) throw new AggregateImportError(this._errors);
82+
}
83+
6084
/**
6185
* Identify which encoding has the given file.
6286
* To check it, we decode and parse the first line of the file with supported encodings.
@@ -95,10 +119,12 @@ class CsvOrganizationLearnerParser {
95119
if (errors.length) {
96120
const hasDelimiterError = errors.some((error) => error.type === 'Delimiter');
97121
if (hasDelimiterError) {
98-
throw new CsvImportError(ERRORS.BAD_CSV_FORMAT);
122+
this._errors.push(new CsvImportError(ERRORS.BAD_CSV_FORMAT));
99123
}
100124
}
101125

126+
this.throwHasErrors();
127+
102128
return { learnerLines, fields };
103129
}
104130

@@ -121,20 +147,22 @@ class CsvOrganizationLearnerParser {
121147

122148
_checkColumns(parsedColumns) {
123149
// Required columns
124-
const missingMandatoryColumn = this._columns
125-
.filter((c) => c.isRequired)
126-
.find((c) => !parsedColumns.includes(c.name));
150+
const mandatoryColumn = this._columns.filter((c) => c.isRequired);
127151

128-
if (missingMandatoryColumn) {
129-
throw new CsvImportError(ERRORS.HEADER_REQUIRED, { field: missingMandatoryColumn.name });
130-
}
152+
mandatoryColumn.forEach((colum) => {
153+
if (!parsedColumns.includes(colum.name)) {
154+
this._errors.push(new CsvImportError(ERRORS.HEADER_REQUIRED, { field: colum.name }));
155+
}
156+
});
131157

132158
// Expected columns
133159
const acceptedColumns = this._columns.map((column) => column.name);
134160

135-
if (_atLeastOneParsedColumnDoesNotMatchAcceptedColumns(parsedColumns, acceptedColumns)) {
136-
throw new CsvImportError(ERRORS.HEADER_UNKNOWN);
137-
}
161+
const unknowColumns = parsedColumns.filter((columnName) => !acceptedColumns.includes(columnName));
162+
163+
unknowColumns.forEach((columnName) => {
164+
if (columnName !== '') this._errors.push(new CsvImportError(ERRORS.HEADER_UNKNOWN, { field: columnName }));
165+
});
138166
}
139167

140168
_buildDateAttribute(dateString) {
@@ -147,41 +175,37 @@ class CsvOrganizationLearnerParser {
147175
return convertedDate || dateString;
148176
}
149177

150-
_handleError(err, index) {
151-
const column = this._columns.find((column) => column.property === err.key);
152-
const line = index + 2;
153-
const field = column.name;
154-
if (err.why === 'min_length') {
155-
throw new CsvImportError(ERRORS.FIELD_MIN_LENGTH, { line, field, limit: err.limit });
156-
}
157-
if (err.why === 'max_length') {
158-
throw new CsvImportError(ERRORS.FIELD_MAX_LENGTH, { line, field, limit: err.limit });
159-
}
160-
if (err.why === 'length') {
161-
throw new CsvImportError(ERRORS.FIELD_LENGTH, { line, field, limit: err.limit });
162-
}
163-
if (err.why === 'date_format' || err.why === 'not_a_date') {
164-
throw new CsvImportError(ERRORS.FIELD_DATE_FORMAT, { line, field });
165-
}
166-
if (err.why === 'email_format') {
167-
throw new CsvImportError(ERRORS.FIELD_EMAIL_FORMAT, { line, field });
168-
}
169-
if (err.why === 'required') {
170-
throw new CsvImportError(ERRORS.FIELD_REQUIRED, { line, field });
171-
}
172-
if (err.why === 'bad_values') {
173-
throw new CsvImportError(ERRORS.FIELD_BAD_VALUES, { line, field, valids: err.valids });
174-
}
175-
throw err;
176-
}
177-
}
178+
_handleValidationError(errors, index) {
179+
errors.forEach((err) => {
180+
const column = this._columns.find((column) => column.property === err.key);
181+
const line = index + 2;
182+
const field = column.name;
178183

179-
function _atLeastOneParsedColumnDoesNotMatchAcceptedColumns(parsedColumns, acceptedColumns) {
180-
return parsedColumns.some((parsedColumn) => {
181-
if (parsedColumn !== '') {
182-
return !acceptedColumns.includes(parsedColumn);
183-
}
184-
});
184+
if (err.why === 'min_length') {
185+
this._errors.push(new CsvImportError(ERRORS.FIELD_MIN_LENGTH, { line, field, limit: err.limit }));
186+
}
187+
if (err.why === 'max_length') {
188+
this._errors.push(new CsvImportError(ERRORS.FIELD_MAX_LENGTH, { line, field, limit: err.limit }));
189+
}
190+
if (err.why === 'length') {
191+
this._errors.push(new CsvImportError(ERRORS.FIELD_LENGTH, { line, field, limit: err.limit }));
192+
}
193+
if (err.why === 'date_format' || err.why === 'not_a_date') {
194+
this._errors.push(new CsvImportError(ERRORS.FIELD_DATE_FORMAT, { line, field }));
195+
}
196+
if (err.why === 'email_format') {
197+
this._errors.push(new CsvImportError(ERRORS.FIELD_EMAIL_FORMAT, { line, field }));
198+
}
199+
if (err.why === 'required') {
200+
this._errors.push(new CsvImportError(ERRORS.FIELD_REQUIRED, { line, field }));
201+
}
202+
if (err.why === 'bad_values') {
203+
this._errors.push(new CsvImportError(ERRORS.FIELD_BAD_VALUES, { line, field, valids: err.valids }));
204+
}
205+
206+
if (!this._supportedErrors.includes(err.why)) this._errors.push(err);
207+
});
208+
}
185209
}
186210

187211
export { CsvOrganizationLearnerParser };

0 commit comments

Comments
 (0)