UNPKG

validated-changeset

Version:
1,585 lines (1,283 loc) 112 kB
import { Changeset } from '../src'; import get from '../src/utils/get-deep'; import set from '../src/utils/set-deep'; import lookupValidator from '../src/utils/validator-lookup'; let dummyModel: any; const exampleArray: Array<any> = []; const dummyValidations: Record<string, any> = { age(_k: string, value: any) { return !!value ? value < 120 || '' : true; }, name(_k: string, value: any) { return (!!value && value.length > 3) || 'too short'; }, email(_k: string, value: any) { const errors = []; if (value && value.length < 5) { errors.push('too short'); } if (value && !value.includes('@')) { errors.push('not an email'); } if (errors.length < 1) { return true; } return errors.length > 1 ? errors : errors[0]; }, password(_k: string, value: unknown) { return !!value || ['foo', 'bar']; }, passwordConfirmation( _k: string, newValue: unknown, _oldValue: unknown, { password: changedPassword }: { password: string }, { password }: { password: string } ) { return ( (!!newValue && (changedPassword === newValue || password === newValue)) || "password doesn't match" ); }, async(_k: string, value: unknown) { return Promise.resolve(value || ''); }, options(_k: string, value: unknown) { return !!value; }, org: { isCompliant(_k: string, value: unknown) { return !!value || 'not provided'; }, usa: { ny: [ (_k: string, value: unknown) => { return !!value || 'must be present'; }, (_k: string, value: string) => { return /[A-z]/.test(value) ? true : 'only letters work'; } ] } }, size: { value(_k: string, value: unknown) { return typeof value === 'number' || 'not a valid size.value'; } } }; function dummyValidator({ key, newValue, oldValue, changes, content }: { key: string; newValue: unknown; oldValue: unknown; changes: any; content: any; }) { const validatorFn = get(dummyValidations, key); if (typeof validatorFn === 'function') { return validatorFn(newValue, oldValue, changes, content); } } describe('Unit | Utility | changeset', () => { beforeEach(() => { dummyModel = { save() { return Promise.resolve(this); }, exampleArray }; }); afterEach(() => { dummyModel = {}; }); /** * #toString */ it('content can be an empty hash', () => { expect.assertions(1); const emptyObject = {}; const dummyChangeset = Changeset(emptyObject, lookupValidator(dummyValidations)); expect(dummyChangeset.toString()).toEqual('changeset:[object Object]'); }); /** * #error */ it('#error returns the error object and keeps changes', () => { const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); const expectedResult = { name: { validation: 'too short', value: 'a' } }; dummyChangeset.set('name', 'a'); expect(dummyChangeset.error).toEqual(expectedResult); expect(dummyChangeset.error.name).toEqual(expectedResult.name); expect(dummyChangeset.get('error.name')).toEqual(expectedResult.name); expect(dummyChangeset.change).toEqual({ name: 'a' }); expect(dummyChangeset.change.name).toEqual('a'); expect(dummyChangeset.get('change.name')).toEqual('a'); }); it('#error can use custom validator', () => { const dummyChangeset = Changeset(dummyModel, dummyValidator); const expectedResult = { name: { validation: 'too short', value: 'a' } }; dummyChangeset.set('name', 'a'); expect(dummyChangeset.error).toEqual(expectedResult); expect(dummyChangeset.error.name).toEqual(expectedResult.name); expect(dummyChangeset.get('error.name')).toEqual(expectedResult.name); expect(dummyChangeset.change).toEqual({ name: 'a' }); expect(dummyChangeset.change.name).toEqual('a'); expect(dummyChangeset.get('change.name')).toEqual('a'); }); it('can get nested values in the error object', function () { const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); const expectedResult = { validation: 'too short', value: 'a' }; dummyChangeset.set('name', 'a'); expect(dummyChangeset.get('error.name')).toEqual(expectedResult); }); it('can can work with an array of nested validations', function () { const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); const expectedResult = { validation: ['too short', 'not an email'], value: 'a' }; dummyChangeset.set('email', 'a'); expect(dummyChangeset.get('error.email')).toEqual(expectedResult); }); /** * #change */ it('#change returns the changes object', () => { const dummyChangeset = Changeset(dummyModel); const expectedResult = { name: 'a' }; dummyChangeset.set('name', 'a'); expect(dummyChangeset.change).toEqual(expectedResult); }); it('#change supports `undefined`', () => { const model = { name: 'a' }; const dummyChangeset = Changeset(model); const expectedResult = { name: undefined }; dummyChangeset.set('name', undefined); expect(dummyChangeset.change).toEqual(expectedResult); }); it('#change works with arrays', () => { const dummyChangeset = Changeset(dummyModel); const newArray = [...exampleArray, 'new']; const expectedResult = { exampleArray: newArray }; dummyChangeset.set('exampleArray', newArray); expect(dummyChangeset.change).toEqual(expectedResult); }); /** * #errors */ it('#errors returns the error object and keeps changes', () => { const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); let expectedResult = [{ key: 'name', validation: 'too short', value: 'a' }]; dummyChangeset.set('name', 'a'); expect(dummyChangeset.errors).toEqual(expectedResult); expect(dummyChangeset.get('errors')).toEqual(expectedResult); }); it('can get nested values in the errors object', () => { const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); dummyChangeset.set('unknown', 'wat'); dummyChangeset.set('org.usa.ny', ''); dummyChangeset.set('name', ''); let expectedErrors = [ { key: 'org.usa.ny', validation: ['must be present', 'only letters work'], value: '' }, { key: 'name', validation: 'too short', value: '' } ]; expect(dummyChangeset.get('errors')).toEqual(expectedErrors); dummyChangeset.set('org.usa.ny', '1'); expectedErrors = [ { key: 'org.usa.ny', validation: ['only letters work'], value: '1' }, { key: 'name', validation: 'too short', value: '' } ]; expect(dummyChangeset.get('errors')).toEqual(expectedErrors); }); /** * #changes */ /** * #data */ it('data reads the changeset CONTENT', () => { const dummyChangeset = Changeset(dummyModel); expect(dummyChangeset.data).toEqual(dummyModel); }); /** * #isValid */ it('does not validate with default options', () => { const dummyChangeset = Changeset( dummyModel, lookupValidator(dummyValidations), dummyValidations, {} ); expect(dummyChangeset.get('isValid')).toBeTruthy(); }); it('does not validate when the initValidate option is set to false', () => { const dummyChangeset = Changeset( dummyModel, lookupValidator(dummyValidations), dummyValidations, { initValidate: false } ); expect(dummyChangeset.get('isValid')).toBeTruthy(); }); it('validates when the initValidate option is set to true', () => { const dummyChangeset = Changeset( dummyModel, lookupValidator(dummyValidations), dummyValidations, { initValidate: true } ); expect(dummyChangeset.get('isValid')).toBeFalsy(); }); it('validates when the initValidate option is set to true and the initial changeset data is valid', () => { dummyModel.name = 'Bobby'; let validations: Record<string, any> = { name(_k: string, value: any) { return (!!value && value.length > 3) || 'too short'; } }; const dummyChangeset = Changeset(dummyModel, lookupValidator(validations), validations, { initValidate: true }); expect(dummyChangeset.get('isValid')).toBeTruthy(); }); /** * #isInvalid */ /** * #isPristine */ it("isPristine returns true if changes are equal to content's values", () => { dummyModel.name = 'Bobby'; dummyModel.thing = 123; dummyModel.nothing = null; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); dummyChangeset.set('name', 'Bobby'); dummyChangeset.set('nothing', null); expect(dummyChangeset.get('isPristine')).toBeTruthy(); }); it("isPristine returns false if changes are not equal to content's values", () => { dummyModel.name = 'Bobby'; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); dummyChangeset.set('name', 'Bobby'); dummyChangeset.set('thing', 123); expect(dummyChangeset.get('isPristine')).toBeFalsy(); }); it('isPristine works with `null` values', () => { dummyModel.name = null; dummyModel.age = 15; const dummyChangeset = Changeset(dummyModel); expect(dummyChangeset.get('isPristine')).toBeTruthy(); dummyChangeset.set('name', 'Kenny'); expect(dummyChangeset.get('isPristine')).toBeFalsy(); dummyChangeset.set('name', null); expect(dummyChangeset.get('isPristine')).toBeTruthy(); }); it('isPristine returns true if changes not in user provided changesetKeys', () => { dummyModel.name = 'Bobby'; dummyModel.thing = 123; dummyModel.nothing = null; const changesetKeys = ['name']; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, { changesetKeys }); dummyChangeset.set('nothing', 'something'); expect(dummyChangeset.get('isPristine')).toBe(true); expect(dummyChangeset.get('isDirty')).toBe(false); }); it('isPristine returns true if nested changes not in user provided changesetKeys', () => { dummyModel.obj = { name: 'Bobby' }; const changesetKeys = ['name']; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, { changesetKeys }); dummyChangeset.set('obj.name', 'something'); expect(dummyChangeset.get('isPristine')).toBe(true); expect(dummyChangeset.get('isDirty')).toBe(false); }); it('isPristine returns false if set a key in changesetKeys', () => { dummyModel.name = 'Bobby'; dummyModel.thing = 123; dummyModel.nothing = null; const changesetKeys = ['razataz']; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, { changesetKeys }); dummyChangeset.set('razataz', 'boom'); expect(dummyChangeset.get('isPristine')).toBe(false); expect(dummyChangeset.get('isDirty')).toBe(true); }); it('isPristine returns false if nested changes in user provided changesetKeys', () => { const changesetKeys = ['org']; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, { changesetKeys }); dummyChangeset.set('org.usa.ny', 'NYE'); expect(dummyChangeset.get('isPristine')).toBe(false); expect(dummyChangeset.get('isDirty')).toBe(true); }); it('isPristine returns true if nested path does not match at the deepest level', () => { const changesetKeys = ['org.usa.ny']; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations), null, { changesetKeys }); dummyChangeset.set('org.usa', 'USA'); expect(dummyChangeset.get('isPristine')).toBe(true); expect(dummyChangeset.get('isDirty')).toBe(false); }); /** * #isDirty */ it('#set dirties changeset', () => { const dummyChangeset = Changeset(dummyModel); dummyChangeset.set('name', 'foo'); expect(dummyChangeset.isDirty).toBe(true); }); it('#isDirty after set', () => { const dummyChangeset = Changeset(dummyModel); dummyChangeset.set('name', 'foo'); expect(dummyChangeset.isDirty).toBe(true); }); it('#isDirty reset after execute', () => { dummyModel.name = {}; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = { short: 'foo' }; expect(dummyChangeset.get('isDirty')).toBe(true); dummyChangeset.execute(); expect(dummyChangeset.get('isDirty')).toBe(false); }); it('#isDirty reset after rollback', () => { dummyModel.name = {}; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = { short: 'foo' }; expect(dummyChangeset.get('isDirty')).toBe(true); dummyChangeset.rollback(); expect(dummyChangeset.get('isDirty')).toBe(false); }); it('#isDirty is false when no set', () => { dummyModel['name'] = { nick: 'bar' }; const dummyChangeset = Changeset(dummyModel); dummyChangeset.name; expect(dummyChangeset.isDirty).toBe(false); }); it('#isDirty is false when no set with deep values', () => { dummyModel['details'] = { name: { nick: 'bar' } }; const dummyChangeset = Changeset(dummyModel); dummyChangeset.get('details.name'); expect(dummyChangeset.isDirty).toBe(false); expect(dummyChangeset.change).toEqual({}); }); it('#isDirty is true when set with deep values', () => { class Dog { value: any; constructor(value: any) { this.value = value; } } dummyModel['details'] = { name: {} }; const dummyChangeset = Changeset(dummyModel); dummyChangeset.get('details.name'); const dogKlass = new Dog({ nickname: 'bar' }); dummyChangeset['details'] = { name: dogKlass }; expect(dummyChangeset.isDirty).toBe(true); expect(dummyChangeset.change).toEqual({ details: { name: dogKlass } }); }); it('#set does not dirty changeset with same date', () => { dummyModel.createTime = new Date('2013-05-01'); const dummyChangeset = Changeset(dummyModel); dummyChangeset.set('createTime', new Date('2013-05-01')); expect(dummyChangeset.isDirty).toBe(false); }); /** * #get */ it('#get proxies to content', () => { dummyModel.name = 'Jim Bob'; const dummyChangeset = Changeset(dummyModel); const result = dummyChangeset.name; expect(result).toBe('Jim Bob'); }); it('#get proxies to content prototype', () => { class Dog { name?: string; } Dog.prototype.name = 'Jim Bob'; const dummyChangeset = Changeset(new Dog()); const result = dummyChangeset.name; expect(result).toBe('Jim Bob'); }); it('#get returns the content when the proxied content is a class', () => { class Moment { date: unknown; constructor(date: Date) { this.date = date; } } const d = new Date('2015'); const momentInstance = new Moment(d); const c = Changeset({ startDate: momentInstance }); const newValue = c.get('startDate'); expect(newValue.date).toEqual(momentInstance.date); expect(newValue.content instanceof Moment).toBeTruthy(); expect(newValue.date).toBe(d); }); it('#get handles changes that are non primitives', () => { class Moment { _isUTC: any; date: unknown; constructor(date: Date) { this.date = date; this._isUTC = false; } } const d = new Date('2015'); const momentInstance = new Moment(d); momentInstance._isUTC = true; const c = Changeset({ startDate: momentInstance }); let newValue = c.get('startDate'); expect(newValue.date).toEqual(momentInstance.date); expect(newValue.content instanceof Moment).toBeTruthy(); expect(newValue.date).toBe(d); expect(newValue._isUTC).toEqual(true); const newD = new Date('2020'); const newMomentInstance = new Moment(newD); c.set('startDate', newMomentInstance); newValue = c.get('startDate'); newMomentInstance._isUTC = undefined; expect(newValue).toEqual(newMomentInstance); expect(newValue instanceof Moment).toBeTruthy(); expect(newValue.date).toBe(newD); expect(newValue._isUTC).toBeUndefined(); }); it('#get merges sibling keys from CONTENT with CHANGES', () => { class Moment { _isUTC: boolean; date: unknown; constructor(date: Date) { this.date = date; this._isUTC = false; } } const d = new Date('2015'); const momentInstance = new Moment(d); momentInstance._isUTC = true; const c = Changeset({ startDate: momentInstance }); let newValue = c.get('startDate'); expect(newValue.date).toEqual(momentInstance.date); expect(newValue.content instanceof Moment).toBeTruthy(); expect(newValue.date).toBe(d); expect(newValue._isUTC).toEqual(true); const newD = new Date('2020'); c.set('startDate.date', newD); newValue = c.get('startDate'); expect(newValue.date).toEqual(newD); expect(newValue.content instanceof Moment).toBeTruthy(); expect(newValue.date).toBe(newD); expect(newValue._isUTC).toBe(true); }); it('#get returns change if present', () => { dummyModel.name = 'Jim Bob'; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = 'Milton Waddams'; const result = dummyChangeset.name; expect(result).toBe('Milton Waddams'); }); it('#get handles change without Change value', () => { const shallowValidations = { ...dummyValidations }; shallowValidations.profile = (_k: string, newValue: any) => { return typeof newValue === 'undefined' ? 'Cannot be blank' : true; }; dummyModel.profile = {}; const c = Changeset(dummyModel, lookupValidator(shallowValidations)); c.profile; c.validate('profile'); let result: any = c.error.profile; expect(result).toBe(undefined); c.set('profile', undefined); c.validate('profile'); result = c.error.profile.validation; expect(result).toBe('Cannot be blank'); }); it('#get returns change that is a blank value', () => { dummyModel.name = 'Jim Bob'; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = ''; const result = dummyChangeset.name; expect(result).toBe(''); }); it('#get returns change that is has undefined as value', () => { dummyModel.name = 'Jim Bob'; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = undefined; const result = dummyChangeset.name; expect(result).toBeUndefined(); }); it('#get nested objects can contain arrays', () => { expect.assertions(7); dummyModel.name = 'Bob'; dummyModel.contact = { emails: ['bob@email.com', 'the_bob@email.com'] }; expect(get(dummyModel, 'contact.emails')).toEqual(['bob@email.com', 'the_bob@email.com']); const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); expect(dummyChangeset.get('name')).toBe('Bob'); expect(dummyChangeset.get('contact.emails')).toEqual(['bob@email.com', 'the_bob@email.com']); dummyChangeset.set('contact.emails', ['fred@email.com', 'the_fred@email.com']); expect(dummyChangeset.get('contact.emails')).toEqual(['fred@email.com', 'the_fred@email.com']); dummyChangeset.rollback(); expect(dummyChangeset.get('contact.emails')).toEqual(['bob@email.com', 'the_bob@email.com']); dummyChangeset.set('contact.emails', ['fred@email.com', 'the_fred@email.com']); expect(dummyChangeset.get('contact.emails')).toEqual(['fred@email.com', 'the_fred@email.com']); dummyChangeset.execute(); expect(dummyModel.contact.emails).toEqual(['fred@email.com', 'the_fred@email.com']); }); it('#getted Object proxies to underlying method', () => { class Dog { breed: string; constructor(b: string) { this.breed = b; } bark() { return `woof i'm a ${this.breed}`; } } const model: Record<string, any> = { foo: { bar: { dog: new Dog('shiba inu, wow') } } }; { const c = Changeset(model); const actual = c.get('foo.bar.dog'); const expectedResult = "woof i'm a shiba inu, wow"; expect(actual.bark()).toEqual(expectedResult); } { const c = Changeset(model); const actual = get(c, 'foo.bar.dog'); const expectedResult = get(model, 'foo.bar.dog'); expect(actual).toEqual(expectedResult); } { const c = Changeset(model); const actual = get(c, 'foo.bar.dog'); const expectedResult = get(model, 'foo.bar.dog'); expect(actual).toEqual(expectedResult); } }); it('#get proxies to underlying array properties', () => { dummyModel.users = ['user1', 'user2']; const dummyChangeset = Changeset(dummyModel); expect((dummyChangeset.users as Array<string>).length).toBe(2); }); it('#get works if content is undefined for nested key', () => { const model: Record<string, any> = {}; const c = Changeset(model); c.set('foo.bar.cat', { color: 'red' }); const cat = c.get('foo.bar.cat'); expect(cat.color).toEqual('red'); }); it('#get works with toString override', () => { dummyModel.toString = function () { return 'mine'; }; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = undefined; const result = dummyChangeset.toString(); expect(result).toEqual('changeset:mine'); }); it('#get prioritizes own methods/getters', () => { dummyModel.trigger = function (arg: any) { expect(arg).toEqual('mine'); }; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = undefined; dummyChangeset.trigger('mine'); }); /** * #set */ it('#set adds a change if valid', () => { const expectedChanges = [{ key: 'name', value: 'foo' }]; const dummyChangeset = Changeset(dummyModel); dummyChangeset.set('name', 'foo'); const changes = dummyChangeset.changes; expect(dummyModel.name).toBeUndefined(); expect(dummyChangeset.get('name')).toEqual('foo'); expect(changes).toEqual(expectedChanges); expect(dummyChangeset.isDirty).toBe(true); expect(dummyChangeset.change).toEqual({ name: 'foo' }); }); it('#set adds a change with plain assignment without existing values', () => { dummyModel['name'] = { nick: 'bar' }; const dummyChangeset = Changeset(dummyModel); const proxy: any = dummyChangeset.name; proxy['nick'] = 'foo'; expect(dummyChangeset.get('name.nick')).toEqual('foo'); const expectedChanges = [{ key: 'name.nick', value: 'foo' }]; const changes = dummyChangeset.changes; expect(changes).toEqual(expectedChanges); }); it('#set adds a change with plain assignment', () => { dummyModel['name'] = 'bar'; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = 'foo'; const changes = dummyChangeset.changes; expect(dummyModel.name).toBe('bar'); expect(dummyChangeset.name).toEqual('foo'); const expectedChanges = [{ key: 'name', value: 'foo' }]; expect(changes).toEqual(expectedChanges); }); it('#set adds a date', () => { const d = new Date(); const expectedChanges = [{ key: 'dateOfBirth', value: d }]; const dummyChangeset = Changeset(dummyModel); dummyChangeset.set('dateOfBirth', d); const changes = dummyChangeset.changes; expect(dummyModel.dateOfBirth).toBeUndefined(); expect(dummyChangeset.get('dateOfBirth')).toEqual(d); expect(changes).toEqual(expectedChanges); }); it('#set adds a date if already set on model', () => { const model = { dateOfBirth: new Date() }; const dummyChangeset = Changeset(model); const d = new Date('March 25, 1990'); dummyChangeset.set('dateOfBirth', d); const changes = dummyChangeset.changes; expect(dummyModel.dateOfBirth).toBeUndefined(); expect(dummyChangeset.get('dateOfBirth')).toEqual(d); expect(dummyChangeset.dateOfBirth).toEqual(d); const expectedChanges = [{ key: 'dateOfBirth', value: d }]; expect(changes).toEqual(expectedChanges); }); it('#set Ember.set works', () => { const expectedChanges = [{ key: 'name', value: 'foo' }]; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = 'foo'; expect(dummyModel.name).toBeUndefined(); expect(dummyChangeset.get('name')).toBe('foo'); const changes = dummyChangeset.changes; expect(changes).toEqual(expectedChanges); dummyChangeset.execute(); expect(dummyModel.name).toBe('foo'); expect(dummyChangeset.get('name')).toBe('foo'); }); it('#set works for nested', () => { const expectedChanges = [{ key: 'name', value: { short: 'foo' } }]; dummyModel.name = {}; dummyModel.org = {}; const dummyChangeset = Changeset(dummyModel); dummyChangeset['name'] = { short: 'foo' }; expect(dummyChangeset.get('name.short')).toBe('foo'); expect(dummyModel.name).toEqual({}); const changes = dummyChangeset.changes; expect(changes).toEqual(expectedChanges); expect(dummyChangeset.name).toEqual({ short: 'foo' }); expect(dummyChangeset.org).toEqual({}); dummyChangeset.execute(); expect(dummyModel.name.short).toBe('foo'); }); it('#set overrides', () => { const expectedChanges = [{ key: 'age', value: '90' }]; let dummyChangeset = Changeset({ age: '10' }); dummyChangeset.set('age', '80'); dummyChangeset.set('age', '10'); dummyChangeset.set('age', '90'); const changes = dummyChangeset.changes; expect(dummyModel.age).toBeUndefined(); expect(dummyChangeset.get('age')).toEqual('90'); expect(changes).toEqual(expectedChanges); expect(dummyChangeset.isDirty).toBe(true); expect(dummyChangeset.change).toEqual({ age: '90' }); }); test('#set Ember.set with Object actually does work TWICE for nested', () => { set(dummyModel, 'name', {}); let title1 = { id: 'Mr', description: 'Mister' }; let title2 = { id: 'Mrs', description: 'Missus' }; let dummyChangeset: any = Changeset(dummyModel); set(dummyChangeset, 'name.title', title1); expect(get(dummyModel, 'name.title.id')).toBeUndefined(); expect(dummyChangeset.name.title.id).toEqual('Mr'); expect(dummyChangeset.get('name.title.id')).toEqual('Mr'); let changes = get(dummyChangeset, 'changes'); expect(changes).toEqual([{ key: 'name.title', value: title1 }]); set(dummyChangeset, 'name.title', title2); expect(get(dummyModel, 'name.title.id')).toBeUndefined(); expect(dummyChangeset.name.title.id).toEqual('Mrs'); expect(dummyChangeset.get('name.title.id')).toEqual('Mrs'); changes = get(dummyChangeset, 'changes'); expect(changes).toEqual([{ key: 'name.title', value: title2 }]); dummyChangeset.execute(); expect(dummyModel.name.title.id).toEqual('Mrs'); }); test('#set with Object should work TWICE for nested', () => { set(dummyModel, 'name', {}); let title1 = { id: 'Mr', description: 'Mister' }; let title2 = { id: 'Mrs', description: 'Missus' }; let dummyChangeset: any = Changeset(dummyModel); dummyChangeset.set('name.title', title1); expect(get(dummyModel, 'name.title.id')).toBeUndefined(); expect(dummyChangeset.name.title.id).toEqual('Mr'); expect(dummyChangeset.get('name.title.id')).toEqual('Mr'); let changes = dummyChangeset.changes; expect(changes).toEqual([{ key: 'name.title', value: title1 }]); dummyChangeset.set('name.title', title2); expect(get(dummyModel, 'name.title.id')).toBeUndefined(); expect(dummyChangeset.name.title.id).toEqual('Mrs'); expect(dummyChangeset.get('name.title.id')).toEqual('Mrs'); changes = dummyChangeset.changes; expect(changes).toEqual([{ key: 'name.title', value: title2 }]); dummyChangeset.execute(); expect(dummyModel.name.title.id).toEqual('Mrs'); }); describe('arrays within nested objects', () => { describe('#set', () => { let initialData: { contact: { emails?: string[] } } = { contact: { emails: [] } }; beforeEach(() => { initialData = { contact: { emails: ['bob@email.com'] } }; }); it('works with boolean values', () => { let initialData = { contact: { emails: [{}, {}] } }; const changeset = Changeset(initialData); changeset.set('contact.emails.2', { nested: false }); expect(changeset.get('contact.emails.2')).toEqual({ nested: false }); changeset.set('contact.emails.2.nested', true); expect(changeset.get('contact.emails.2')).toEqual({ nested: true }); changeset.set('contact.emails.2.nested', false); expect(changeset.get('contact.emails.2')).toEqual({ nested: false }); }); it('nested objects cannot create arrays when we have no hints', () => { initialData.contact = {}; const changeset = Changeset(initialData); expect(changeset.get('contact.emails')).toEqual(undefined); changeset.set('contact.emails.0', 'fred@email.com'); expect(changeset.get('contact.emails.0')).toEqual('fred@email.com'); expect(changeset.get('contact.emails')).toEqual({ '0': 'fred@email.com' }); }); it('works with validations', () => { const changeset = Changeset( initialData, lookupValidator({ contact: { emails: [ (_k: string, value: any) => { if (value.includes('fred')) { return 'Fred is banned'; } } ] } }) ); expect(changeset.isValid).toEqual(true); changeset.set('contact.emails.0', 'fred@email.com'); expect(changeset.isValid).toEqual(false); expect(changeset.isDirty).toEqual(true); expect(changeset.errors).toEqual([ { key: 'contact.emails.0', validation: 'Fred is banned', value: 'fred@email.com' } ]); }); it('can be rolled back', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.0', 'fred@email.com'); expect(changeset.get('contact.emails.0')).toEqual('fred@email.com'); expect(changeset.changes).toEqual([{ key: 'contact.emails.0', value: 'fred@email.com' }]); expect(changeset.get('contact.emails').unwrap()).toEqual(['fred@email.com']); changeset.rollback(); expect(changeset.get('contact.emails.0')).toEqual('bob@email.com'); expect(changeset.changes).toEqual([]); expect(changeset.get('contact.emails')).toEqual(['bob@email.com']); }); it('can be saved', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.0', 'fred@email.com'); expect(changeset.get('contact.emails.0')).toEqual('fred@email.com'); expect(changeset.changes).toEqual([{ key: 'contact.emails.0', value: 'fred@email.com' }]); changeset.save(); expect(changeset.get('contact.emails.0')).toEqual('fred@email.com'); expect(changeset.changes).toEqual([]); }); it('can add items to the array', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.1', 'fred@email.com'); expect(changeset.get('contact.emails.1')).toEqual('fred@email.com'); expect(changeset.get('contact.emails').unwrap()).toEqual([ 'bob@email.com', 'fred@email.com' ]); expect(changeset.changes).toEqual([{ key: 'contact.emails.1', value: 'fred@email.com' }]); changeset.set('contact.emails.3', 'greg@email.com'); expect(changeset.get('contact.emails.3')).toEqual('greg@email.com'); expect(changeset.get('contact.emails').unwrap()).toEqual([ 'bob@email.com', 'fred@email.com', undefined, 'greg@email.com' ]); expect(changeset.changes).toEqual([ { key: 'contact.emails.1', value: 'fred@email.com' }, { key: 'contact.emails.3', value: 'greg@email.com' } ]); expect(changeset.change).toEqual({ contact: { emails: { 1: 'fred@email.com', 3: 'greg@email.com' } } }); }); it('can remove items from the array', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.1', 'fred@email.com'); expect(changeset.get('contact.emails.1')).toEqual('fred@email.com'); expect(changeset.get('contact.emails').unwrap()).toEqual([ 'bob@email.com', 'fred@email.com' ]); expect(changeset.changes).toEqual([{ key: 'contact.emails.1', value: 'fred@email.com' }]); changeset.set('contact.emails.0', null); expect(changeset.get('contact.emails.0')).toEqual(null); expect(changeset.get('contact.emails').unwrap()).toEqual([null, 'fred@email.com']); expect(changeset.changes).toEqual([ { key: 'contact.emails.0', value: null }, { key: 'contact.emails.1', value: 'fred@email.com' } ]); changeset.set('contact.emails.1', null); expect(changeset.get('contact.emails').unwrap()).toEqual([null, null]); expect(changeset.changes).toEqual([ { key: 'contact.emails.0', value: null }, { key: 'contact.emails.1', value: null } ]); }); it('can add an item to an index in an array where that item was previously removed', () => { const deepObj = (email: string) => ({ emails: { primary: email } }); const bob = deepObj('bob@email.com'); const fred = deepObj('fred@email.com'); const sanHolo = deepObj('sanholo@email.com'); const changeset = Changeset({ contacts: [bob, fred] }); // "Delete" array element changeset.set('contacts.0', null); expect(changeset.isDirty).toBeTruthy(); expect(changeset.get('contacts.0')).toEqual(null); expect(changeset.get('contacts')).toEqual([null, fred]); expect(changeset.changes).toEqual([{ key: 'contacts.0', value: null }]); // Set array element to entirely new object changeset.set('contacts.0', sanHolo); expect(changeset.isDirty).toBeTruthy(); expect(changeset.get('contacts')).toEqual([sanHolo, fred]); expect(changeset.get('contacts.0.emails.primary')).toEqual('sanholo@email.com'); expect(changeset.changes).toEqual([{ key: 'contacts.0', value: sanHolo }]); // "Delete" array element again changeset.set('contacts.0', null); expect(changeset.isDirty).toBeTruthy(); expect(changeset.get('contacts.0')).toEqual(null); expect(changeset.get('contacts')).toEqual([null, fred]); expect(changeset.changes).toEqual([{ key: 'contacts.0', value: null }]); // Revert everything changeset.rollback(); expect(changeset.isDirty).toBeFalsy(); expect(changeset.changes).toEqual([]); expect(changeset.get('contacts')).toEqual([bob, fred]); }); xit(`negative values are not allowed`, () => { // This test is currently disabled because setDeep doesn't have a reference to the // original array and setDeep is where we'd throw on invalid key values const changeset = Changeset(initialData); expect(changeset.get('contact.emails')).toEqual(['bob@email.com']); expect(() => { changeset.set('contact.emails.-1', 'fred@email.com'); }).toThrow( 'Negative indices are not allowed as arrays do not serialize values at negative indices' ); }); }); }); describe('arrays as values of top level objects', () => { let initialData: { emails: Record<string, string>[] } = { emails: [] }; beforeEach(() => { initialData = { emails: [{ primary: 'bob@email.com' }] }; }); it('can modify properties on an entry', () => { const changeset = Changeset(initialData); changeset.set('emails.0.primary', 'fun@email.com'); expect(changeset.get('emails.0.primary')).toEqual('fun@email.com'); expect(changeset.get('emails')).toEqual([{ primary: 'fun@email.com' }]); expect(changeset.changes).toEqual([{ key: 'emails.0.primary', value: 'fun@email.com' }]); }); it('can add properties to an entry', () => { const changeset = Changeset(initialData); changeset.set('emails.0.funEmail', 'fun@email.com'); expect(changeset.get('emails.0.funEmail')).toEqual('fun@email.com'); expect(changeset.changes).toEqual([{ key: 'emails.0.funEmail', value: 'fun@email.com' }]); expect(changeset.get('emails')).toEqual([ { primary: 'bob@email.com', funEmail: 'fun@email.com' } ]); }); it('can add new properties to new entries', () => { const changeset = Changeset(initialData); changeset.set('emails.1.funEmail', 'fun@email.com'); changeset.set('emails.1.primary', 'primary@email.com'); expect(changeset.get('emails.1.funEmail')).toEqual('fun@email.com'); expect(changeset.get('emails.1.primary')).toEqual('primary@email.com'); expect(changeset.get('emails')).toEqual([ { primary: 'bob@email.com' }, { primary: 'primary@email.com', funEmail: 'fun@email.com' } ]); expect(changeset.changes).toEqual([ { key: 'emails.1.funEmail', value: 'fun@email.com' }, { key: 'emails.1.primary', value: 'primary@email.com' } ]); }); it('can add a new object all at once, and edit it', () => { const changeset = Changeset(initialData); changeset.set('emails.1', { funEmail: 'fun@email.com', primary: 'primary@email.com' }); expect(changeset.get('emails.1.funEmail')).toEqual('fun@email.com'); expect(changeset.get('emails.1.primary')).toEqual('primary@email.com'); expect(changeset.get('emails')).toEqual([ { primary: 'bob@email.com' }, { primary: 'primary@email.com', funEmail: 'fun@email.com' } ]); expect(changeset.changes).toEqual([ { key: 'emails.1', value: { funEmail: 'fun@email.com', primary: 'primary@email.com' } } ]); changeset.set('emails.1.primary', 'primary2@email.com'); expect(changeset.get('emails.1.primary')).toEqual('primary2@email.com'); expect(changeset.changes).toEqual([ { key: 'emails.1', value: { primary: 'primary2@email.com', funEmail: 'fun@email.com' } } ]); expect(changeset.get('emails')).toEqual([ { primary: 'bob@email.com' }, { primary: 'primary2@email.com', funEmail: 'fun@email.com' } ]); }); it('can edit a new object that was added after deleting an array entry', () => { const changeset = Changeset({ emails: [ { fun: 'fun0@email.com', primary: 'primary0@email.com' }, { fun: 'fun1@email.com', primary: 'primary1@email.com' } ] }); changeset.set('emails.1', null); expect(changeset.get('emails.0.fun')).toEqual('fun0@email.com'); expect(changeset.get('emails.0.primary')).toEqual('primary0@email.com'); expect(changeset.get('emails')).toEqual([ { fun: 'fun0@email.com', primary: 'primary0@email.com' }, null ]); expect(changeset.changes).toEqual([ { key: 'emails.1', value: null } ]); changeset.set('emails.1', { fun: 'brandNew@email.com', primary: 'brandNewPrimary@email.com' }); expect(changeset.get('emails')).toEqual([ { fun: 'fun0@email.com', primary: 'primary0@email.com' }, { fun: 'brandNew@email.com', primary: 'brandNewPrimary@email.com' } ]); expect(changeset.changes).toEqual([ { key: 'emails.1', value: { fun: 'brandNew@email.com', primary: 'brandNewPrimary@email.com' } } ]); }); it('can edit an object with a key of value after another array entry has been deleted', () => { const changeset = Changeset({ emails: [ { fun: 'fun0@email.com', primary: 'primary0@email.com', value: 'the value' }, { fun: 'fun1@email.com', primary: 'primary1@email.com', value: 'some value' } ] }); changeset.set('emails.1', null); expect(changeset.get('emails')).toEqual([ { fun: 'fun0@email.com', primary: 'primary0@email.com', value: 'the value' }, null ]); expect(changeset.changes).toEqual([ { key: 'emails.1', value: null } ]); expect(changeset.get('emails.0.fun')).toEqual('fun0@email.com'); expect(changeset.get('emails.0.primary')).toEqual('primary0@email.com'); // does not need to be unwrapped expect(changeset.get('emails.0.value')).toEqual('the value'); }); }); describe('arrays of objects within nested objects', () => { describe('#set', () => { let initialData: { contact: { emails: Record<string, string>[] } } = { contact: { emails: [] } }; beforeEach(() => { initialData = { contact: { emails: [{ primary: 'bob@email.com' }] } }; }); it('can modify properties on an entry', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.0.primary', 'fun@email.com'); expect(changeset.get('contact.emails.0.primary')).toEqual('fun@email.com'); expect(changeset.get('contact.emails').unwrap()).toEqual([{ primary: 'fun@email.com' }]); expect(changeset.changes).toEqual([ { key: 'contact.emails.0.primary', value: 'fun@email.com' } ]); }); it('can add properties to an entry', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.0.funEmail', 'fun@email.com'); expect(changeset.get('contact.emails.0.funEmail')).toEqual('fun@email.com'); expect(changeset.changes).toEqual([ { key: 'contact.emails.0.funEmail', value: 'fun@email.com' } ]); expect(changeset.get('contact.emails').unwrap()).toEqual([ { primary: 'bob@email.com', funEmail: 'fun@email.com' } ]); }); it('can add new properties to new entries', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.1.funEmail', 'fun@email.com'); changeset.set('contact.emails.1.primary', 'primary@email.com'); expect(changeset.get('contact.emails.1.funEmail')).toEqual('fun@email.com'); expect(changeset.get('contact.emails.1.primary')).toEqual('primary@email.com'); expect(changeset.get('contact.emails').unwrap()).toEqual([ { primary: 'bob@email.com' }, { primary: 'primary@email.com', funEmail: 'fun@email.com' } ]); expect(changeset.changes).toEqual([ { key: 'contact.emails.1.funEmail', value: 'fun@email.com' }, { key: 'contact.emails.1.primary', value: 'primary@email.com' } ]); }); it('can add a new object all at once, and edit it', () => { const changeset = Changeset(initialData); changeset.set('contact.emails.1', { funEmail: 'fun@email.com', primary: 'primary@email.com' }); expect(changeset.get('contact.emails.1.funEmail')).toEqual('fun@email.com'); expect(changeset.get('contact.emails.1.primary')).toEqual('primary@email.com'); expect(changeset.get('contact.emails').unwrap()).toEqual([ { primary: 'bob@email.com' }, { primary: 'primary@email.com', funEmail: 'fun@email.com' } ]); expect(changeset.changes).toEqual([ { key: 'contact.emails.1', value: { funEmail: 'fun@email.com', primary: 'primary@email.com' } } ]); changeset.set('contact.emails.1.primary', 'primary2@email.com'); expect(changeset.get('contact.emails.1.primary')).toEqual('primary2@email.com'); expect(changeset.changes).toEqual([ { key: 'contact.emails.1', value: { primary: 'primary2@email.com', funEmail: 'fun@email.com' } } ]); expect(changeset.get('contact.emails').unwrap()).toEqual([ { primary: 'bob@email.com' }, { primary: 'primary2@email.com', funEmail: 'fun@email.com' } ]); }); it('can edit a new object that was added after deleting an array entry', () => { const changeset = Changeset({ contacts: { emails: [ { fun: 'fun0@email.com', primary: 'primary0@email.com' }, { fun: 'fun1@email.com', primary: 'primary1@email.com' } ] } }); changeset.set('contacts.emails.1', null); expect(changeset.get('contacts.emails').unwrap()).toEqual([ { fun: 'fun0@email.com', primary: 'primary0@email.com' }, null ]); }); }); }); it('#set works for nested when the root key is "value"', () => { dummyModel.value = {}; dummyModel.org = {}; const dummyChangeset = Changeset(dummyModel); dummyChangeset.set('value.short', 'foo'); expect(dummyChangeset.get('value.short')).toBe('foo'); expect(dummyModel.value).toEqual({}); const changes = dummyChangeset.changes; const expectedChanges = [{ key: 'value.short', value: 'foo' }]; expect(changes).toEqual(expectedChanges); expect(dummyChangeset.value).toEqual({ short: 'foo' }); expect(dummyChangeset.org).toEqual({}); dummyChangeset.execute(); expect(dummyModel.value.short).toBe('foo'); }); it('nested objects can be replaced with different ones without changing the nested return values', () => { dummyModel['org'] = { usa: { ny: 'ny' } }; const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); dummyChangeset.set('org', { usa: { ca: 'ca' } }); expect(dummyChangeset.get('org')).toEqual({ usa: { ca: 'ca', ny: undefined } }); expect(dummyChangeset.get('org.usa')).toEqual({ ca: 'ca', ny: undefined }); expect(dummyChangeset.get('org.usa.ca')).toBe('ca'); expect(dummyChangeset.get('org.usa.ny')).toBeUndefined(); }); it('nested objects can be replaced with different ones as classes', () => { class Country { details: object; constructor(details: object) { this.details = details; } } dummyModel['org'] = new Country({ usa: { ny: 'ny' } }); const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); dummyChangeset.set('org', new Country({ usa: { ca: 'ca' } })); expect(dummyChangeset.get('org')).toEqual(new Country({ usa: { ca: 'ca', ny: undefined } })); expect(dummyChangeset.get('org.details')).toEqual({ usa: { ca: 'ca', ny: undefined } }); expect(dummyChangeset.get('org.details.usa')).toEqual({ ca: 'ca', ny: undefined }); expect(dummyChangeset.get('org.details.usa.ca')).toBe('ca'); expect(dummyChangeset.get('org.details.usa.ny')).toBeUndefined(); }); it('#set doesnt lose sibling keys', () => { dummyModel['org'] = { usa: { mn: 'mn', ny: 'ny', nz: 'nz' }, landArea: 100 }; const c: Record<string, any> = Changeset(dummyModel); c.set('org.usa.ny', 'NY'); expect(dummyModel.org.usa.ny).toBe('ny'); expect(c.org.usa.ny).toBe('NY'); expect(c.get('org.usa.ny')).toBe('NY'); expect(c.get('org.usa.mn')).toBe('mn'); expect(c.get('org.usa.nz')).toBe('nz'); expect(c.get('org.landArea')).toBe(100); // set again c.set('org.usa.ny', 'nye'); expect(dummyModel.org.usa.ny).toBe('ny'); expect(c.org.usa.ny).toBe('nye'); expect(c.get('org.usa.ny')).toBe('nye'); expect(c.get('org.usa.mn')).toBe('mn'); expect(c.get('org.usa.nz')).toBe('nz'); expect(c.get('org.landArea')).toBe(100); }); it('#set adds a change if the key is an object', () => { dummyModel['org'] = { usa: { mn: 'mn', ny: 'ny', nz: 'nz' }, landArea: 100 }; const c: any = Changeset(dummyModel); c.set('org.usa.ny', 'NY'); expect(dummyModel.org.usa.ny).toBe('ny'); expect(c.org.usa.ny).toBe('NY'); expect(c.get('org.usa.ny')).toBe('NY'); expect(c.get('org.usa.mn')).toBe('mn'); expect(c.get('org.usa.nz')).toBe('nz'); expect(c.get('org.landArea')).toBe(100); const expectedChanges = [{ key: 'org.usa.ny', value: 'NY' }]; const changes = c.changes; expect(changes).toEqual(expectedChanges); }); it('#set use native setters with nested doesnt work', () => { dummyModel['org'] = { usa: { ny: 'ny' } }; const c = Changeset(dummyModel); set(c, 'org.usa.ny', 'foo'); expect(dummyModel.org.usa.ny).toBe('foo'); expect(c.get('org.usa.ny')).toBe('foo'); const changes = c.changes; expect(changes).toEqual([]); }); it('#set use native setters at single level', () => { dummyModel.org = 'ny'; const c = Changeset(dummyModel); c.org = 'foo'; expect(dummyModel.org).toBe('ny'); expect(c.or