diff --git a/package.json b/package.json index 99fe853..280e1a9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "index.d.ts", "scripts": { "test": "mocha -r ts-node/register test/**/*.test.ts", - "test:coverage": "nyc npm run test", + "test:coverage": "NODE_OPTIONS='--max-old-space-size=8192' nyc npm run test", "test:watch": "nodemon -e '*' --watch test --watch src --exec npm run test:coverage", "commit": "cz", "feature-tests": "./node_modules/.bin/cucumber-js -p default", @@ -66,6 +66,7 @@ }, "devDependencies": { "@cucumber/cucumber": "^8.0.0-rc.1", + "@date-fns/utc": "^1.2.0", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/async-lock": "^1.1.3", "@types/chai": "^4.2.22", @@ -95,6 +96,7 @@ }, "dependencies": { "async-lock": "^1.3.0", + "date-fns": "^3.6.0", "get-random-values": "^1.2.2", "lodash": "^4.17.21" } diff --git a/src/properties.ts b/src/properties.ts index a5b7934..c8ec362 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -1,5 +1,6 @@ import merge from 'lodash/merge' import get from 'lodash/get' +import { formatDate } from 'date-fns/format' import { createPropertyValidator, isType, @@ -41,6 +42,8 @@ import { const EMAIL_REGEX = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/u +const DEFAULT_JUST_DATE_FORMAT = 'yyyy-MM-dd' + const Property = < TValue extends Arrayable, T extends FunctionalModel = FunctionalModel, @@ -128,8 +131,36 @@ const Property = < return r } +/** + * Config object for a date property + */ +type DatePropertyConfig = PropertyConfig & { + /** + * A date-fns format. + */ + format?: string + isDateOnly?: boolean +} + +/** + * Determines if the value is a Date object. + * @param value Any value + */ +const isDate = (value: any): value is Date => { + if (value === null) { + return false + } + return typeof value === 'object' && value.toISOString +} + +/** + * A Property for Dates. Both strings and Date objects. + * @param config + * @param additionalMetadata + * @constructor + */ const DateProperty = >( - config: PropertyConfig = {}, + config: DatePropertyConfig = {}, additionalMetadata = {} ) => Property( @@ -137,11 +168,24 @@ const DateProperty = >( merge( { lazyLoadMethod: (value: Arrayable) => { - if (!value && config?.autoNow) { - return new Date() + if (isDate(value)) { + if (config.format) { + return formatDate(value, config.format) + } + if (config.isDateOnly) { + return formatDate(value, DEFAULT_JUST_DATE_FORMAT) + } + return value.toISOString() } - if (typeof value === 'string') { - return new Date(value) + if (!value && config?.autoNow) { + const date = new Date() + if (config.format) { + return formatDate(date, config.format) + } + if (config.isDateOnly) { + return formatDate(date, DEFAULT_JUST_DATE_FORMAT) + } + return date.toISOString() } return value }, diff --git a/test/src/properties.test.ts b/test/src/properties.test.ts index a50e9cd..d613dfd 100644 --- a/test/src/properties.test.ts +++ b/test/src/properties.test.ts @@ -1,6 +1,7 @@ import { assert } from 'chai' import chai from 'chai' import sinon from 'sinon' +import { UTCDate } from '@date-fns/utc' import asPromised from 'chai-as-promised' import { UniqueId, @@ -1176,7 +1177,7 @@ describe('/src/properties.ts', () => { {} as unknown as ModelInstance ) const actual = await instance() - const expected = date + const expected = date.toISOString() assert.deepEqual(actual, expected) }) it('should return null, if null config is passed and no value created.', async () => { @@ -1192,18 +1193,100 @@ describe('/src/properties.ts', () => { const expected = null assert.deepEqual(actual, expected) }) - it('should return a Date object when a string date is passed in', async () => { + it('should return a string when a string date is passed in', async () => { const proto = DateProperty({}) - const date = '2020-01-01T00:00:01Z' + const date = '2020-01-01T00:00:01.000Z' const instance = proto.createGetter( date, {}, {} as unknown as ModelInstance ) - const actual = ((await instance()) as Date).toISOString() + const actual = await instance() const expected = new Date(date).toISOString() assert.equal(actual, expected) }) + it('should return a string when a date object is passed in', async () => { + const proto = DateProperty({}) + const date = new Date('2020-01-01T00:00:01.000Z') + const instance = proto.createGetter( + date, + {}, + {} as unknown as ModelInstance + ) + const actual = typeof (await instance()) + const expected = 'string' + assert.equal(actual, expected) + }) + it('should return a string when a date object is passed in', async () => { + const proto = DateProperty({}) + const date = new Date('2020-01-01T00:00:01.000Z') + const instance = proto.createGetter( + date, + {}, + {} as unknown as ModelInstance + ) + const actual = typeof (await instance()) + const expected = 'string' + assert.equal(actual, expected) + }) + it('should return the expected string when a date object is passed in and format is provided', async () => { + const proto = DateProperty({ + format: 'MMMM', + }) + const date = new UTCDate('2020-01-01T00:00:01.000Z') + const instance = proto.createGetter( + date, + {}, + {} as unknown as ModelInstance + ) + const actual = await instance() + const expected = 'January' + assert.equal(actual, expected) + }) + it('should return the expected date only string when a date object is passed in and isDateOnly:true', async () => { + const proto = DateProperty({ + isDateOnly: true, + }) + const date = new UTCDate('2020-01-01T00:00:01.000Z') + const instance = proto.createGetter( + date, + {}, + {} as unknown as ModelInstance + ) + const actual = await instance() + const expected = '2020-01-01' + assert.equal(actual, expected) + }) + it('should a date string when autoNow, isDateOnly=true and no value is provided', async () => { + const proto = DateProperty({ + isDateOnly: true, + autoNow: true, + }) + const instance = proto.createGetter( + null, + {}, + {} as unknown as ModelInstance + ) + const actual = (await instance()) as string + const re = /[0-9]{4}-[0-9]{2}-[0-9]{2}/g + const found = actual.match(re) + assert.isOk(found) + }) + it('should formatted string when autoNow, format=yyyy and no value is provided', async () => { + const proto = DateProperty({ + format: 'yyyy', + autoNow: true, + }) + const instance = proto.createGetter( + null, + {}, + {} as unknown as ModelInstance + ) + const actual = (await instance()) as string + const re = /[0-9]{4}/g + const found = actual.match(re) + assert.isOk(found) + }) }) describe('#AdvancedModelReferenceProperty()', () => { it('should not throw an exception when a custom Model is passed in', () => {