Skip to content

Commit 560bbac

Browse files
authored
Merge pull request #25 from storyofams/feat/parse-date-pipe
[feat] parse date pipe
2 parents ce3d047 + ea0037a commit 560bbac

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

lib/e2e.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createHandler } from './createHandler';
77
import { Body, Delete, Get, Header, HttpCode, Post, Put, Query, Req, Res, Response, SetHeader } from './decorators';
88
import { ValidationPipe } from './pipes';
99
import { ParseBooleanPipe } from './pipes/parseBoolean.pipe';
10+
import { ParseDatePipe } from './pipes/parseDate.pipe';
1011
import { ParseNumberPipe } from './pipes/parseNumber.pipe';
1112

1213
enum CreateSource {
@@ -49,9 +50,18 @@ class TestHandler {
4950
@Header('Content-Type') contentType: string,
5051
@Query('id') id: string,
5152
@Query('step', ParseNumberPipe({ nullable: false })) step: number,
52-
@Query('redirect', ParseBooleanPipe) redirect: boolean
53+
@Query('redirect', ParseBooleanPipe) redirect: boolean,
54+
@Query('startAt', ParseDatePipe) startAt: Date
5355
) {
54-
return { contentType, id, step, redirect, test: this.testField };
56+
return {
57+
contentType,
58+
id,
59+
step,
60+
redirect,
61+
test: this.testField,
62+
startAt,
63+
isStartAtDateInstance: startAt instanceof Date
64+
};
5565
}
5666

5767
@HttpCode(201)
@@ -94,7 +104,7 @@ describe('E2E', () => {
94104

95105
it('read', () =>
96106
request(server)
97-
.get('/?id=my-id&step=1&redirect=true')
107+
.get('/?id=my-id&step=1&redirect=true&startAt=2021-01-01T22:00:00')
98108
.set('Content-Type', 'application/json')
99109
.expect(200)
100110
.then(res =>
@@ -108,7 +118,8 @@ describe('E2E', () => {
108118
contentType: 'application/json',
109119
id: 'my-id',
110120
step: 1,
111-
redirect: true
121+
redirect: true,
122+
isStartAtDateInstance: true
112123
}
113124
})
114125
));

lib/pipes/parseDate.pipe.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ParseDatePipe } from './parseDate.pipe';
2+
3+
describe('ParseDatePipe', () => {
4+
it('Should parse the given date', () => {
5+
const parsed = ParseDatePipe()('2021-01-01');
6+
expect(parsed).toBeInstanceOf(Date);
7+
expect(parsed).toEqual(new Date('2021-01-01'));
8+
});
9+
10+
it('Should parse the given date and time string with spaces in between.', () => {
11+
const parsed = ParseDatePipe()('2021-01-01 20:00:00');
12+
expect(parsed).toBeInstanceOf(Date);
13+
expect(parsed).toEqual(new Date('2021-01-01T20:00:00'));
14+
});
15+
16+
it('Should parse the given date and time string with T in between.', () => {
17+
const parsed = ParseDatePipe()('2021-01-01T20:00:00');
18+
expect(parsed).toBeInstanceOf(Date);
19+
expect(parsed).toEqual(new Date('2021-01-01T20:00:00'));
20+
});
21+
22+
it('Should throw when then given value is not a string.', () => expect(() => ParseDatePipe()(1)).toThrow());
23+
24+
it('Should pass for partial date (year and month).', () =>
25+
expect(ParseDatePipe()('2021-01')).toEqual(new Date('2021-01-01')));
26+
27+
it('Should throw for non-existing date.', () => expect(() => ParseDatePipe()('2021-02-31')).toThrow());
28+
29+
it('Should throw when the given value is string `null`.', () => {
30+
expect(() => ParseDatePipe()('null')).toThrow();
31+
});
32+
33+
it('Should throw when the given value is `null`.', () => {
34+
expect(() => ParseDatePipe()(null)).toThrow();
35+
});
36+
});

lib/pipes/parseDate.pipe.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { BadRequestException } from '../exceptions';
2+
import type { PipeMetadata, PipeOptions } from './ParameterPipe';
3+
import { validatePipeOptions } from './validatePipeOptions';
4+
5+
// The following variables and functions are taken from the validator.js (https://github.com/validatorjs/validator.js/blob/master/src/lib/isISO8601.js)
6+
7+
// from http://goo.gl/0ejHHW
8+
const iso8601 = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
9+
// same as above, except with a strict 'T' separator between date and time
10+
const iso8601StrictSeparator = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
11+
12+
const isValidDate = (str: string) => {
13+
// str must have passed the ISO8601 check
14+
// this check is meant to catch invalid dates
15+
// like 2009-02-31
16+
const match = (str.match(/(\d{4})-?(\d{0,2})-?(\d*)/) ?? []).map(Number);
17+
const year = match[1];
18+
const month = match[2];
19+
const day = match[3];
20+
const monthString = month ? `0${month}`.slice(-2) : month;
21+
const dayString = day ? `0${day}`.slice(-2) : day;
22+
23+
// create a date object and compare
24+
const d = new Date(`${year}-${monthString || '01'}-${dayString || '01'}`);
25+
if (month && day) {
26+
return d.getUTCFullYear() === year && d.getUTCMonth() + 1 === month && d.getUTCDate() === day;
27+
}
28+
29+
return true;
30+
};
31+
32+
function isISO8601(str: string, options: { strictSeparator?: boolean; strict?: boolean } = {}) {
33+
const check = options.strictSeparator ? iso8601StrictSeparator.test(str) : iso8601.test(str);
34+
35+
if (check && options.strict) {
36+
return isValidDate(str);
37+
}
38+
39+
return check;
40+
}
41+
42+
export function ParseDatePipe(options?: PipeOptions) {
43+
return (value: any, metadata?: PipeMetadata) => {
44+
validatePipeOptions(value, metadata?.name, options);
45+
46+
if (value && !isISO8601(value, { strict: true })) {
47+
throw new BadRequestException(
48+
`Validation failed${metadata?.name ? ` for ${metadata.name}` : ''} (date string is expected)`
49+
);
50+
}
51+
52+
return new Date(value);
53+
};
54+
}

0 commit comments

Comments
 (0)