Skip to content

Commit

Permalink
Add dateFrom, dateTo validation to CalendarDay (#3002)
Browse files Browse the repository at this point in the history
* Add dateFrom, dateTo validation to CalendarDay
  • Loading branch information
timleslie authored Jun 10, 2020
1 parent 6f84f53 commit b693b2f
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 89 deletions.
20 changes: 20 additions & 0 deletions .changeset/spotty-planes-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@keystonejs/api-tests': patch
'@keystonejs/demo-project-blog': patch
'@keystonejs/demo-project-meetup': patch
'@arch-ui/day-picker': patch
'@keystonejs/fields': major
---

The `CalendarDay` field type options `yearRangeFrom` and `yearRangeTo` have been removed, and replaced with `dateFrom` and `dateTo`. These options take an ISO8601 formatted date string in the form `YYYY-MM-DD`, e.g. `2020-06-30`. These values are now validated on the server-side, rather than just on the client-side within the Admin UI.

If you are currently using `yearRangeFrom` or `yearRangeTo` you will need to make the following change:

```
birthday: { type: CalendarDay, yearRangeFrom: 1900, yearRangeTo: 2100 }
```
becomes

```
birthday: { type: CalendarDay, dateFrom: '1900-01-01', dateTo: '2100-12-31' }
```
71 changes: 71 additions & 0 deletions api-tests/CalendarDay.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const { multiAdapterRunners, setupServer, graphqlRequest } = require('@keystonejs/test-utils');

const { CalendarDay } = require('@keystonejs/fields');
const cuid = require('cuid');

function setupKeystone(adapterName) {
return setupServer({
adapterName,
name: `ks5-testdb-${cuid()}`,
createLists: keystone => {
keystone.createList('User', {
fields: {
birthday: { type: CalendarDay, dateFrom: '2000-01-01', dateTo: '2020-01-01' },
},
});
},
});
}

multiAdapterRunners().map(({ runner, adapterName }) =>
describe(`Adapter: ${adapterName}`, () => {
describe('CalendarDay type', () => {
test(
'Valid date passes validation',
runner(setupKeystone, async ({ keystone }) => {
const { data } = await graphqlRequest({
keystone,
query: `mutation { createUser(data: { birthday: "2001-01-01" }) { birthday } }`,
});

expect(data).toHaveProperty('createUser.birthday', '2001-01-01');
})
);

test(
'date === dateTo passes validation',
runner(setupKeystone, async ({ keystone }) => {
const { data } = await graphqlRequest({
keystone,
query: `mutation { createUser(data: { birthday: "2020-01-01" }) { birthday } }`,
});
expect(data).toHaveProperty('createUser.birthday', '2020-01-01');
})
);

test(
'date === dateFrom passes validation',
runner(setupKeystone, async ({ keystone }) => {
const { data } = await graphqlRequest({
keystone,
query: `mutation { createUser(data: { birthday: "2020-01-01" }) { birthday } }`,
});
expect(data).toHaveProperty('createUser.birthday', '2020-01-01');
})
);

test(
'Invalid date failsvalidation',
runner(setupKeystone, async ({ keystone }) => {
const { errors } = await graphqlRequest({
keystone,
query: `mutation { createUser(data: { birthday: "3000-01-01" }) { birthday } }`,
});
expect(errors).toHaveLength(1);
const error = errors[0];
expect(error.message).toEqual('You attempted to perform an invalid mutation');
})
);
});
})
);
1 change: 1 addition & 0 deletions api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@keystonejs/test-utils": "^6.1.4",
"@keystonejs/utils": "^5.4.1",
"cuid": "^2.1.8",
"date-fns": "^2.14.0",
"express": "^4.17.1"
}
}
2 changes: 1 addition & 1 deletion demo-projects/blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"apollo-client": "^2.6.10",
"apollo-upload-client": "^13.0.0",
"cross-env": "^7.0.0",
"date-fns": "^2.13.0",
"date-fns": "^2.14.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"graphql-tag": "^2.10.3",
Expand Down
6 changes: 3 additions & 3 deletions demo-projects/blog/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const {
const { Wysiwyg } = require('@keystonejs/fields-wysiwyg-tinymce');
const { AuthedRelationship } = require('@keystonejs/fields-authed-relationship');
const { LocalFileAdapter } = require('@keystonejs/file-adapters');
const getYear = require('date-fns/getYear');
const { formatISO } = require('date-fns');

const { staticRoute, staticPath, distDir } = require('./config');
const dev = process.env.NODE_ENV !== 'production';
Expand Down Expand Up @@ -45,8 +45,8 @@ exports.User = {
dob: {
type: CalendarDay,
format: 'do MMMM yyyy',
yearRangeFrom: 1901,
yearRangeTo: getYear(new Date()),
dateFrom: '1901-01-01',
dateTo: formatISO(new Date(), { representation: 'date' }),
},
...(process.env.IFRAMELY_API_KEY
? {
Expand Down
2 changes: 1 addition & 1 deletion demo-projects/meetup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"apollo-upload-client": "^13.0.0",
"body-parser": "^1.18.2",
"cross-env": "^7.0.0",
"date-fns": "^2.13.0",
"date-fns": "^2.14.0",
"dotenv": "^8.2.0",
"eslint-plugin-emotion": "^10.0.27",
"express": "^4.17.1",
Expand Down
3 changes: 2 additions & 1 deletion demo-projects/todo/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { Keystone } = require('@keystonejs/keystone');
const { MongooseAdapter } = require('@keystonejs/adapter-mongoose');
const { Text } = require('@keystonejs/fields');
const { Text, CalendarDay } = require('@keystonejs/fields');
const { GraphQLApp } = require('@keystonejs/app-graphql');
const { AdminUIApp } = require('@keystonejs/app-admin-ui');
const { StaticApp } = require('@keystonejs/app-static');
Expand All @@ -14,6 +14,7 @@ keystone.createList('Todo', {
schemaDoc: 'A list of things which need to be done',
fields: {
name: { type: Text, schemaDoc: 'This is the thing you need to do', isRequired: true },
day: { type: CalendarDay, dateTo: '2000-01-01', dateFrom: '2000-01-01' },
},
});

Expand Down
2 changes: 1 addition & 1 deletion packages/arch/packages/day-picker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@emotion/core": "^10.0.28",
"@emotion/styled": "^10.0.27",
"chrono-node": "^1.4.6",
"date-fns": "^2.13.0",
"date-fns": "^2.14.0",
"intersection-observer": "^0.10.0",
"moment": "^2.24.0",
"react-window": "^1.7.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/fields/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"apollo-errors": "^1.9.0",
"bcryptjs": "^2.4.3",
"cuid": "^2.1.8",
"date-fns": "^2.13.0",
"date-fns": "^2.14.0",
"dumb-passwords": "^0.2.1",
"google-maps-react": "^2.0.2",
"graphql": "^14.6.0",
Expand Down
68 changes: 52 additions & 16 deletions packages/fields/src/types/CalendarDay/Implementation.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { formatISO, parseISO } from 'date-fns';
import { formatISO, parseISO, compareAsc, compareDesc, isValid } from 'date-fns';
import { Implementation } from '../../Implementation';
import { MongooseFieldAdapter } from '@keystonejs/adapter-mongoose';
import { KnexFieldAdapter } from '@keystonejs/adapter-knex';

export class CalendarDay extends Implementation {
constructor(
path,
{
format = 'yyyy-MM-dd',
yearRangeFrom = new Date().getFullYear() - 100,
yearRangeTo = new Date().getFullYear(),
}
) {
constructor(path, { format = 'yyyy-MM-dd', dateFrom, dateTo }) {
super(...arguments);
this.format = format;
this.yearRangeFrom = yearRangeFrom;
this.yearRangeTo = yearRangeTo;
this._dateFrom = dateFrom;
this._dateTo = dateTo;

if (this._dateFrom && (this._dateFrom.length !== 10 || !isValid(parseISO(this._dateFrom)))) {
throw new Error(
`Invalid value for option "dateFrom" of field '${this.listKey}.${path}': "${this._dateFrom}"`
);
}

if (this._dateTo && (this._dateTo.length !== 10 || !isValid(parseISO(this._dateTo)))) {
throw new Error(
`Invalid value for option "dateTo" of field '${this.listKey}.${path}': "${this._dateFrom}"`
);
}

if (
this._dateTo &&
this._dateFrom &&
compareAsc(parseISO(this._dateFrom), parseISO(this._dateTo)) === 1
) {
throw new Error(
`Invalid values for options "dateFrom", "dateTo" of field '${this.listKey}.${path}': "${dateFrom}" > "${dateTo}"`
);
}
this.isOrderable = true;
}

Expand Down Expand Up @@ -43,10 +57,33 @@ export class CalendarDay extends Implementation {
return {
...meta,
format: this.format,
yearRangeFrom: this.yearRangeFrom,
yearRangeTo: this.yearRangeTo,
dateFrom: this._dateFrom,
dateTo: this._dateTo,
};
}

async validateInput({ resolvedData, addFieldValidationError }) {
const initialValue = resolvedData[this.path];
const parsedValue = parseISO(resolvedData[this.path]);

if (!(initialValue.length === 10 && isValid(parsedValue))) {
addFieldValidationError('Invalid CalendarDay value', { value: resolvedData[this.path] });
}
if (parsedValue) {
if (parseISO(this._dateFrom) && compareAsc(parseISO(this._dateFrom), parsedValue) === 1) {
addFieldValidationError('Value is before earliest allowed date.', {
value: resolvedData[this.path],
dateFrom: this._dateFromString,
});
}
if (parseISO(this._dateTo) && compareDesc(parseISO(this._dateTo), parsedValue) === 1) {
addFieldValidationError('Value is after latest allowed date.', {
value: resolvedData[this.path],
dateTo: this._dateToString,
});
}
}
}
}

const CommonCalendarInterface = superclass =>
Expand All @@ -62,8 +99,7 @@ const CommonCalendarInterface = superclass =>

export class MongoCalendarDayInterface extends CommonCalendarInterface(MongooseFieldAdapter) {
addToMongooseSchema(schema) {
const validator = a =>
typeof a === 'string' && formatISO(parseISO(a), { representation: 'date' }) === a;
const validator = a => typeof a === 'string' && a.length === 10 && parseISO(a);
const schemaOptions = {
type: String,
validate: {
Expand Down
58 changes: 58 additions & 0 deletions packages/fields/src/types/CalendarDay/Implementation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CalendarDay } from './Implementation';
import { MockAdapter, MockFieldAdapter } from '@keystonejs/test-utils';

const mockAdapter = new MockAdapter();
const mocks = {
getListByKey: () => {},
listKey: '',
listAdapter: mockAdapter.newListAdapter(),
fieldAdapterClass: MockFieldAdapter,
defaultAccess: true,
schemaNames: ['public'],
};

describe('CalendarDay#implementation', () => {
it('Instantiates correctly if dateFrom is before dateTo', () => {
expect(() => {
new CalendarDay('date', { dateFrom: '2000-01-01', dateTo: '2001-01-01' }, mocks);
}).not.toThrow();
});

it('Instantiates correctly with only dateFrom', () => {
expect(() => {
new CalendarDay('date', { dateFrom: '2000-01-01' }, mocks);
}).not.toThrow();
});

it('Instantiates correctly with only dateTo', () => {
expect(() => {
new CalendarDay('date', { dateTo: '2000-01-01' }, mocks);
}).not.toThrow();
});

describe('error handling', () => {
it("throws if 'dateTo' is before 'dateFrom'", () => {
return expect(
() => new CalendarDay('date', { dateTo: '2000-01-01', dateFrom: '2020-01-01', mocks })
).toThrow();
});

it("throws if 'dateTo' === 'dateFrom'", () => {
return expect(
() => new CalendarDay('date', { dateTo: '2020-01-01', dateFrom: '2020-01-01', mocks })
).toThrow();
});

it("throws if 'dateTo' is invalid", () => {
return expect(
() => new CalendarDay('date', { dateTo: '2000--1--1', dateFrom: '2020-01-01', mocks })
).toThrow();
});

it("throws if 'dateFrom' is invalid", () => {
return expect(
() => new CalendarDay('date', { dateFrom: '2000--1--1', dateTo: '2020-01-01', mocks })
).toThrow();
});
});
});
Loading

0 comments on commit b693b2f

Please sign in to comment.