• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nestjs / nest / 4bfe98f8-c530-4503-87ed-d0e96d8d1b74

18 Sep 2023 10:04AM UTC coverage: 92.653% (+0.04%) from 92.61%
4bfe98f8-c530-4503-87ed-d0e96d8d1b74

Pull #12403

circleci

tobias
test(common): added test for fieldname in validation pipes
Pull Request #12403: feat(common): adding the fieldname to validation errors

2435 of 2918 branches covered (0.0%)

4 of 4 new or added lines in 1 file covered. (100.0%)

6469 of 6982 relevant lines covered (92.65%)

16.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.94
/packages/common/pipes/parse-array.pipe.ts
1
import { Injectable } from '../decorators/core/injectable.decorator';
1✔
2
import { Optional } from '../decorators/core/optional.decorator';
1✔
3
import { HttpStatus } from '../enums/http-status.enum';
1✔
4
import { Type } from '../interfaces';
5
import {
6
  ArgumentMetadata,
7
  PipeTransform,
8
} from '../interfaces/features/pipe-transform.interface';
9
import { HttpErrorByCode } from '../utils/http-error-by-code.util';
1✔
10
import { isNil, isUndefined, isString } from '../utils/shared.utils';
1✔
11
import { ValidationPipe, ValidationPipeOptions } from './validation.pipe';
1✔
12

13
const DEFAULT_ARRAY_SEPARATOR = ',';
1✔
14

15
/**
16
 * @publicApi
17
 */
18
export interface ParseArrayOptions
19
  extends Omit<
20
    ValidationPipeOptions,
21
    'transform' | 'validateCustomDecorators' | 'exceptionFactory'
22
  > {
23
  items?: Type<unknown>;
24
  separator?: string;
25
  optional?: boolean;
26
  exceptionFactory?: (error: any) => any;
27
}
28

29
/**
30
 * Defines the built-in ParseArray Pipe
31
 *
32
 * @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
33
 *
34
 * @publicApi
35
 */
36
@Injectable()
37
export class ParseArrayPipe implements PipeTransform {
1✔
38
  protected readonly validationPipe: ValidationPipe;
39
  protected exceptionFactory: (error: string) => any;
40

41
  constructor(@Optional() protected readonly options: ParseArrayOptions = {}) {
22✔
42
    this.validationPipe = new ValidationPipe({
22✔
43
      transform: true,
44
      validateCustomDecorators: true,
45
      ...options,
46
    });
47

48
    const { exceptionFactory, errorHttpStatusCode = HttpStatus.BAD_REQUEST } =
22✔
49
      options;
22✔
50
    this.exceptionFactory =
22✔
51
      exceptionFactory ||
44✔
52
      (error => new HttpErrorByCode[errorHttpStatusCode](error));
15✔
53
  }
54

55
  /**
56
   * Method that accesses and performs optional transformation on argument for
57
   * in-flight requests.
58
   *
59
   * @param value currently processed route argument
60
   * @param metadata contains metadata about the currently processed route argument
61
   */
62
  async transform(value: any, metadata: ArgumentMetadata): Promise<any> {
63
    if (!value && !this.options.optional) {
22✔
64
      throw this.exceptionFactory(
1✔
65
        this.getValidationErrorMessage(metadata.data),
66
      );
67
    } else if (isNil(value) && this.options.optional) {
21✔
68
      return value;
1✔
69
    }
70

71
    if (!Array.isArray(value)) {
20✔
72
      if (!isString(value)) {
16✔
73
        throw this.exceptionFactory(
5✔
74
          this.getValidationErrorMessage(metadata.data),
75
        );
76
      } else {
77
        try {
11✔
78
          value = value
11✔
79
            .trim()
80
            .split(this.options.separator || DEFAULT_ARRAY_SEPARATOR);
16✔
81
        } catch {
82
          throw this.exceptionFactory(
×
83
            this.getValidationErrorMessage(metadata.data),
84
          );
85
        }
86
      }
87
    }
88
    if (this.options.items) {
15✔
89
      const validationMetadata: ArgumentMetadata = {
12✔
90
        metatype: this.options.items,
91
        type: 'query',
92
      };
93

94
      const isExpectedTypePrimitive = this.isExpectedTypePrimitive();
12✔
95
      const toClassInstance = (item: any, index?: number) => {
12✔
96
        if (this.options.items !== String) {
39✔
97
          try {
31✔
98
            item = JSON.parse(item);
31✔
99
          } catch {}
100
        }
101
        if (isExpectedTypePrimitive) {
39✔
102
          return this.validatePrimitive(item, index);
26✔
103
        }
104
        return this.validationPipe.transform(item, validationMetadata);
13✔
105
      };
106
      if (this.options.stopAtFirstError === false) {
12✔
107
        // strict compare to "false" to make sure
108
        // that this option is disabled by default
109
        let errors = [];
4✔
110

111
        const targetArray = value as Array<unknown>;
4✔
112
        for (let i = 0; i < targetArray.length; i++) {
4✔
113
          try {
13✔
114
            targetArray[i] = await toClassInstance(targetArray[i]);
13✔
115
          } catch (err) {
116
            let message: string[] | unknown;
117
            if ((err as any).getResponse) {
9!
118
              const response = (err as any).getResponse();
9✔
119
              if (Array.isArray(response.message)) {
9✔
120
                message = response.message.map(
6✔
121
                  (item: string) => `[${i}] ${item}`,
11✔
122
                );
123
              } else {
124
                message = `[${i}] ${response.message}`;
3✔
125
              }
126
            } else {
127
              message = err;
×
128
            }
129
            errors = errors.concat(message);
9✔
130
          }
131
        }
132
        if (errors.length > 0) {
4!
133
          throw this.exceptionFactory(errors as any);
4✔
134
        }
135
        return targetArray;
×
136
      } else {
137
        value = await Promise.all(value.map(toClassInstance));
8✔
138
      }
139
    }
140
    return value;
9✔
141
  }
142

143
  protected getValidationErrorMessage(field: string) {
144
    return `Validation failed (parsable array expected in "${field}")`;
6✔
145
  }
146

147
  protected isExpectedTypePrimitive(): boolean {
148
    return [Boolean, Number, String].includes(this.options.items as any);
12✔
149
  }
150

151
  protected validatePrimitive(originalValue: any, index?: number) {
152
    if (this.options.items === Number) {
26✔
153
      const value =
154
        originalValue !== null && originalValue !== '' ? +originalValue : NaN;
15✔
155
      if (isNaN(value)) {
15✔
156
        throw this.exceptionFactory(
4✔
157
          `${isUndefined(index) ? '' : `[${index}] `}item must be a number`,
4✔
158
        );
159
      }
160
      return value;
11✔
161
    } else if (this.options.items === String) {
11✔
162
      if (!isString(originalValue)) {
8!
163
        return `${originalValue}`;
×
164
      }
165
    } else if (this.options.items === Boolean) {
3!
166
      if (typeof originalValue !== 'boolean') {
3✔
167
        throw this.exceptionFactory(
1✔
168
          `${
169
            isUndefined(index) ? '' : `[${index}] `
1!
170
          }item must be a boolean value`,
171
        );
172
      }
173
    }
174
    return originalValue;
10✔
175
  }
176
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc