Skip to content
33 changes: 33 additions & 0 deletions lib/Onyx.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,36 @@ function mergeCollection(collectionKey, collection) {
});
}

/**
* Insert API responses and lifecycle data into Onyx
*
* @param {Array} data An array of objects with shape {onyxMethod: oneOf('set', 'merge'), key: string, value: *}
*/
function update(data) {
Copy link
Member

@parasharrajat parasharrajat Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, there is a problem with this method's signature. It should return a Promise like other methods.

When this is passed to

update = decorate.decorateWithMetrics(update, 'Onyx:update');
, it will throw.

decorateWithMetrics expects a promise as a return value for the function.

Tackling here Expensify/App#10622. If we don't want to make this change, then the proposal is looking good on that issue.

Also, metrics module method signatures are broken for the web as well. cc: @luacmartins

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parasharrajat thanks for investigating this. So can we just return the respective methods here? e.g.

case 'merge':
    return merge(key, value);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work. There is a each loop here.

Need to convert to map and return promises and handle those, etc.

// First, validate the Onyx object is in the format we expect
_.each(data, ({onyxMethod, key}) => {
if (!_.contains(['set', 'merge'], onyxMethod)) {
throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
}
if (!_.isString(key)) {
throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we give the index of the key for a more descriptive error message? We may end up with a big object and not know which key is the non-string, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea. We could certainly add that as a follow up if we come across this issue!

}
});

_.each(data, ({onyxMethod, key, value}) => {
switch (onyxMethod) {
case 'set':
set(key, value);
break;
case 'merge':
merge(key, value);
break;
default:
break;
}
});
}

/**
* Initialize the store with actions and listening for storage events
*
Expand Down Expand Up @@ -856,6 +886,7 @@ const Onyx = {
multiSet,
merge,
mergeCollection,
update,
clear,
init,
registerLogger,
Expand Down Expand Up @@ -883,6 +914,7 @@ function applyDecorators() {
mergeCollection = decorate.decorateWithMetrics(mergeCollection, 'Onyx:mergeCollection');
getAllKeys = decorate.decorateWithMetrics(getAllKeys, 'Onyx:getAllKeys');
initializeWithDefaultKeyStates = decorate.decorateWithMetrics(initializeWithDefaultKeyStates, 'Onyx:defaults');
update = decorate.decorateWithMetrics(update, 'Onyx:update');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had missed adding the update method to the decorators for capturing metrics

/* eslint-enable */

// Re-expose decorated methods
Expand All @@ -891,6 +923,7 @@ function applyDecorators() {
Onyx.clear = clear;
Onyx.merge = merge;
Onyx.mergeCollection = mergeCollection;
Onyx.update = update;

// Expose stats methods on Onyx
Onyx.getMetrics = decorate.getMetrics;
Expand Down
80 changes: 80 additions & 0 deletions tests/unit/onyxTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,84 @@ describe('Onyx', () => {
});
});
});

it('should use update data object to set/merge keys', () => {
let testKeyValue;
connectionID = Onyx.connect({
key: ONYX_KEYS.TEST_KEY,
initWithStoredValues: false,
callback: (value) => {
testKeyValue = value;
},
});

let anotherTestKeyValue;
connectionID = Onyx.connect({
key: ONYX_KEYS.ANOTHER_TEST,
initWithStoredValues: false,
callback: (value) => {
anotherTestKeyValue = value;
},
});

return waitForPromisesToResolve()
.then(() => {
// GIVEN the initial Onyx state: {test: true, anotherTest: {test1: 'test1'}}
Onyx.set(ONYX_KEYS.TEST_KEY, true);
Onyx.set(ONYX_KEYS.ANOTHER_TEST, {test1: 'test1'});
return waitForPromisesToResolve();
})
.then(() => {
expect(testKeyValue).toBe(true);
expect(anotherTestKeyValue).toEqual({test1: 'test1'});

// WHEN we pass a data object to Onyx.update
Onyx.update([
{
onyxMethod: 'set',
key: ONYX_KEYS.TEST_KEY,
value: 'one',
},
{
onyxMethod: 'merge',
key: ONYX_KEYS.ANOTHER_TEST,
value: {test2: 'test2'},
},
]);
return waitForPromisesToResolve();
})
.then(() => {
// THEN the final Onyx state should be {test: 'one', anotherTest: {test1: 'test1', test2: 'test2'}}
expect(testKeyValue).toBe('one');
expect(anotherTestKeyValue).toEqual({test1: 'test1', test2: 'test2'});
});
});

it('should throw an error when the data object is incorrect in Onyx.update', () => {
// GIVEN the invalid data object with onyxMethod='multiSet'
const data = [
{onyxMethod: 'set', key: ONYX_KEYS.TEST_KEY, value: 'four'},
{onyxMethod: 'multiSet', key: ONYX_KEYS.ANOTHER_TEST, value: {test2: 'test2'}}
];

try {
// WHEN we pass it to Onyx.update
Onyx.update(data);
} catch (error) {
// THEN we should expect the error message below
expect(error.message).toEqual('Invalid onyxMethod multiSet in Onyx update.');
}

try {
// GIVEN the invalid data object with key=true
data[1] = {onyxMethod: 'merge', key: true, value: {test2: 'test2'}};

// WHEN we pass it to Onyx.update
Onyx.update(data);
} catch (error) {
// THEN we should expect the error message below
// eslint-disable-next-line max-len
expect(error.message).toEqual('Invalid boolean key provided in Onyx update. Onyx key must be of type string.');
}
});
});