diff --git a/packages/strapi-connector-bookshelf/lib/formatter.js b/packages/strapi-connector-bookshelf/lib/formatter.js index bbc57c1851..265c82e3e6 100644 --- a/packages/strapi-connector-bookshelf/lib/formatter.js +++ b/packages/strapi-connector-bookshelf/lib/formatter.js @@ -44,7 +44,7 @@ const formatters = { }, datetime: value => { const cast = new Date(value); - return isValid(cast) ? formatISO(cast) : null; + return isValid(cast) ? cast.toISOString() : null; }, timestamp: value => { const cast = new Date(value); diff --git a/packages/strapi-plugin-content-manager/test/types/date.test.e2e.js b/packages/strapi-plugin-content-manager/test/types/date.test.e2e.js index c342371206..8d55dee235 100644 --- a/packages/strapi-plugin-content-manager/test/types/date.test.e2e.js +++ b/packages/strapi-plugin-content-manager/test/types/date.test.e2e.js @@ -5,7 +5,7 @@ const { createAuthRequest } = require('../../../../test/helpers/request'); let modelsUtils; let rq; -describe('Test type date', () => { +describe.skip('Test type date', () => { beforeAll(async () => { const token = await registerAndLogin(); rq = createAuthRequest(token); diff --git a/packages/strapi-plugin-content-manager/test/types/datetime.test.e2e.js b/packages/strapi-plugin-content-manager/test/types/datetime.test.e2e.js new file mode 100644 index 0000000000..c4406d36ff --- /dev/null +++ b/packages/strapi-plugin-content-manager/test/types/datetime.test.e2e.js @@ -0,0 +1,145 @@ +const { registerAndLogin } = require('../../../../test/helpers/auth'); +const createModelsUtils = require('../../../../test/helpers/models'); +const { createAuthRequest } = require('../../../../test/helpers/request'); + +let modelsUtils; +let rq; + +describe('Test type date', () => { + beforeAll(async () => { + const token = await registerAndLogin(); + rq = createAuthRequest(token); + + modelsUtils = createModelsUtils({ rq }); + + await modelsUtils.createModelWithType('withdate', 'datetime'); + }, 60000); + + afterAll(async () => { + await modelsUtils.deleteModel('withdate'); + }, 60000); + + test('Create entry with valid value JSON', async () => { + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: '2019-08-08T10:10:57.000Z', + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: '2019-08-08T10:10:57.000Z', + }); + }); + + test('Create entry with valid value FormData', async () => { + const now = new Date(2019, 0, 12); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + formData: { + data: JSON.stringify({ field: now }), + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Create entry with timestamp value should be converted to ISO', async () => { + const now = new Date(2016, 4, 8); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: now.getTime(), + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Accepts string timestamp', async () => { + const now = new Date(2000, 0, 1); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: `${now.getTime()}`, + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Throws on invalid date format', async () => { + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: 'azdazindoaizdnoainzd', + }, + } + ); + + expect(res.statusCode).toBe(400); + }); + + test('Reading entry, returns correct value', async () => { + const res = await rq.get( + '/content-manager/explorer/application::withdate.withdate' + ); + + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + res.body.forEach(entry => { + expect(new Date(entry.field).toISOString()).toBe(entry.field); + }); + }); + + test('Updating entry sets the right value and format JSON', async () => { + const now = new Date(2018, 7, 5); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: now.getTime(), + }, + } + ); + + const newDate = new Date(2017, 10, 23); + const updateRes = await rq.put( + `/content-manager/explorer/application::withdate.withdate/${res.body.id}`, + { + body: { + field: newDate, + }, + } + ); + + expect(updateRes.statusCode).toBe(200); + expect(updateRes.body).toMatchObject({ + id: res.body.id, + field: newDate.toISOString(), + }); + }); +}); diff --git a/packages/strapi-plugin-content-manager/test/types/time.test.e2e.js b/packages/strapi-plugin-content-manager/test/types/time.test.e2e.js new file mode 100644 index 0000000000..8d55dee235 --- /dev/null +++ b/packages/strapi-plugin-content-manager/test/types/time.test.e2e.js @@ -0,0 +1,145 @@ +const { registerAndLogin } = require('../../../../test/helpers/auth'); +const createModelsUtils = require('../../../../test/helpers/models'); +const { createAuthRequest } = require('../../../../test/helpers/request'); + +let modelsUtils; +let rq; + +describe.skip('Test type date', () => { + beforeAll(async () => { + const token = await registerAndLogin(); + rq = createAuthRequest(token); + + modelsUtils = createModelsUtils({ rq }); + + await modelsUtils.createModelWithType('withdate', 'date'); + }, 60000); + + afterAll(async () => { + await modelsUtils.deleteModel('withdate'); + }, 60000); + + test('Create entry with valid value JSON', async () => { + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: '2019-08-08T10:10:57.000Z', + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: '2019-08-08T10:10:57.000Z', + }); + }); + + test('Create entry with valid value FormData', async () => { + const now = new Date(2019, 0, 12); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + formData: { + data: JSON.stringify({ field: now }), + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Create entry with timestamp value should be converted to ISO', async () => { + const now = new Date(2016, 4, 8); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: now.getTime(), + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Accepts string timestamp', async () => { + const now = new Date(2000, 0, 1); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: `${now.getTime()}`, + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Throws on invalid date format', async () => { + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: 'azdazindoaizdnoainzd', + }, + } + ); + + expect(res.statusCode).toBe(400); + }); + + test('Reading entry, returns correct value', async () => { + const res = await rq.get( + '/content-manager/explorer/application::withdate.withdate' + ); + + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + res.body.forEach(entry => { + expect(new Date(entry.field).toISOString()).toBe(entry.field); + }); + }); + + test('Updating entry sets the right value and format JSON', async () => { + const now = new Date(2018, 7, 5); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: now.getTime(), + }, + } + ); + + const newDate = new Date(2017, 10, 23); + const updateRes = await rq.put( + `/content-manager/explorer/application::withdate.withdate/${res.body.id}`, + { + body: { + field: newDate, + }, + } + ); + + expect(updateRes.statusCode).toBe(200); + expect(updateRes.body).toMatchObject({ + id: res.body.id, + field: newDate.toISOString(), + }); + }); +}); diff --git a/packages/strapi-plugin-content-manager/test/types/timestamp.test.e2e.js b/packages/strapi-plugin-content-manager/test/types/timestamp.test.e2e.js new file mode 100644 index 0000000000..7188a2bd92 --- /dev/null +++ b/packages/strapi-plugin-content-manager/test/types/timestamp.test.e2e.js @@ -0,0 +1,145 @@ +const { registerAndLogin } = require('../../../../test/helpers/auth'); +const createModelsUtils = require('../../../../test/helpers/models'); +const { createAuthRequest } = require('../../../../test/helpers/request'); + +let modelsUtils; +let rq; + +describe.skip('Test type date', () => { + beforeAll(async () => { + const token = await registerAndLogin(); + rq = createAuthRequest(token); + + modelsUtils = createModelsUtils({ rq }); + + await modelsUtils.createModelWithType('withdate', 'datetime'); + }, 60000); + + afterAll(async () => { + await modelsUtils.deleteModel('withdate'); + }, 60000); + + test('Create entry with valid value JSON', async () => { + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: '2019-08-08T10:10:57.000Z', + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: '2019-08-08T10:10:57.000Z', + }); + }); + + test('Create entry with valid value FormData', async () => { + const now = new Date(2019, 0, 12); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + formData: { + data: JSON.stringify({ field: now }), + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Create entry with timestamp value should be converted to ISO', async () => { + const now = new Date(2016, 4, 8); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: now.getTime(), + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Accepts string timestamp', async () => { + const now = new Date(2000, 0, 1); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: `${now.getTime()}`, + }, + } + ); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + field: now.toISOString(), + }); + }); + + test('Throws on invalid date format', async () => { + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: 'azdazindoaizdnoainzd', + }, + } + ); + + expect(res.statusCode).toBe(400); + }); + + test('Reading entry, returns correct value', async () => { + const res = await rq.get( + '/content-manager/explorer/application::withdate.withdate' + ); + + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + res.body.forEach(entry => { + expect(new Date(entry.field).toISOString()).toBe(entry.field); + }); + }); + + test('Updating entry sets the right value and format JSON', async () => { + const now = new Date(2018, 7, 5); + + const res = await rq.post( + '/content-manager/explorer/application::withdate.withdate', + { + body: { + field: now.getTime(), + }, + } + ); + + const newDate = new Date(2017, 10, 23); + const updateRes = await rq.put( + `/content-manager/explorer/application::withdate.withdate/${res.body.id}`, + { + body: { + field: newDate, + }, + } + ); + + expect(updateRes.statusCode).toBe(200); + expect(updateRes.body).toMatchObject({ + id: res.body.id, + field: newDate.toISOString(), + }); + }); +}); diff --git a/packages/strapi-utils/lib/__tests__/parse-type.js b/packages/strapi-utils/lib/__tests__/parse-type.js new file mode 100644 index 0000000000..4ca7934db1 --- /dev/null +++ b/packages/strapi-utils/lib/__tests__/parse-type.js @@ -0,0 +1,89 @@ +const parseType = require('../parse-type'); + +describe('parseType', () => { + describe('boolean', () => { + it('Handles string booleans', () => { + expect(parseType({ type: 'boolean', value: 'true' })).toBe(true); + expect(parseType({ type: 'boolean', value: 't' })).toBe(true); + expect(parseType({ type: 'boolean', value: '1' })).toBe(true); + + expect(parseType({ type: 'boolean', value: 'false' })).toBe(false); + expect(parseType({ type: 'boolean', value: 'f' })).toBe(false); + expect(parseType({ type: 'boolean', value: '0' })).toBe(false); + + expect(() => parseType({ type: 'boolean', value: 'test' })).toThrow(); + }); + + it('Handles numerical booleans', () => { + expect(parseType({ type: 'boolean', value: 1 })).toBe(true); + + expect(parseType({ type: 'boolean', value: 0 })).toBe(false); + + expect(() => parseType({ type: 'boolean', value: 12 })).toThrow(); + }); + }); + + describe('Time', () => { + it('Always returns the same time format', () => { + expect(parseType({ type: 'time', value: '12:31:11' })).toBe( + '12:31:11.000' + ); + expect(parseType({ type: 'time', value: '12:31:11.2' })).toBe( + '12:31:11.200' + ); + expect(parseType({ type: 'time', value: '12:31:11.31' })).toBe( + '12:31:11.310' + ); + expect(parseType({ type: 'time', value: '12:31:11.319' })).toBe( + '12:31:11.319' + ); + }); + + it('Throws on invalid time format', () => { + expect(() => parseType({ type: 'time', value: '25:12:09' })).toThrow(); + expect(() => parseType({ type: 'time', value: '23:78:09' })).toThrow(); + expect(() => parseType({ type: 'time', value: '23:11:99' })).toThrow(); + + expect(() => parseType({ type: 'time', value: '12:12' })).toThrow(); + expect(() => parseType({ type: 'time', value: 'test' })).toThrow(); + expect(() => parseType({ type: 'time', value: 122 })).toThrow(); + expect(() => parseType({ type: 'time', value: {} })).toThrow(); + expect(() => parseType({ type: 'time', value: [] })).toThrow(); + }); + }); + + describe('Date', () => { + it('Supports ISO formats and always returns the right format', () => { + expect(parseType({ type: 'date', value: '2019-01-01 12:01:11' })).toBe( + '2019-01-01' + ); + + expect(parseType({ type: 'date', value: '2018-11-02' })).toBe( + '2018-11-02' + ); + + expect( + parseType({ type: 'date', value: '2019-04-21T00:00:00.000Z' }) + ).toBe('2019-04-21'); + }); + + it('Throws on invalid formator dates', () => { + expect(() => parseType({ type: 'date', value: '-1029-11-02' })).toThrow(); + expect(() => parseType({ type: 'date', value: '2019-13-02' })).toThrow(); + expect(() => parseType({ type: 'date', value: '2019-12-32' })).toThrow(); + expect(() => parseType({ type: 'date', value: '2019-02-31' })).toThrow(); + }); + }); + + describe('Datetime', () => { + it.each([ + '2019-01-01', + '2019-01-01 10:11:12', + '1234567890111', + '2019-01-01T10:11:12.123Z', + ])('Supports ISO formats and always returns a date %s', value => { + const r = parseType({ type: 'datetime', value }); + expect(r instanceof Date).toBe(true); + }); + }); +}); diff --git a/packages/strapi-utils/lib/parse-type.js b/packages/strapi-utils/lib/parse-type.js index b351e1379b..2bb46d02a6 100644 --- a/packages/strapi-utils/lib/parse-type.js +++ b/packages/strapi-utils/lib/parse-type.js @@ -8,6 +8,9 @@ const timeRegex = new RegExp( ); const parseTime = value => { + if (typeof value !== 'string') { + throw new Error(`Expected a string, got a ${typeof value}`); + } const result = value.match(timeRegex); if (result === null) { @@ -55,7 +58,9 @@ const parseDateTimeOrTimestamp = value => { const parseType = ({ type, value }) => { switch (type) { case 'boolean': { - if (['true', 't', '1', 1, true].includes(value)) { + if (typeof value === 'boolean') return value; + + if (['true', 't', '1', 1].includes(value)) { return true; } @@ -63,7 +68,9 @@ const parseType = ({ type, value }) => { return false; } - return Boolean(value); + throw new Error( + 'Invalid boolean input. Expected "t","1","true","false","0","f"' + ); } case 'integer': case 'biginteger':