Skip to content

Conversation

@tdeekens
Copy link
Contributor

Summary

This changes the internal workings of sync-actions to allow for automatic generation of a changeMasterVariant action.

Description

The sync-actions package treated the masterVariant separately when calculating update actions. This comes with a couple of down sides:

  1. Code duplication across a if (masterVariant) forEach(variants)
  2. It restricts the module in detecting changing the master variant

The latter is the issue this tries to solve while also enabling the the generation of a changeMasterVariant update action. Previously, whenever the masterVariant changed the library would assume the whole variant would need creation and would create a update actions accordingly. While also creating update actions to delete the variant which actually became the new master variant.

The problem with this approach is that it is a bug and a feature at the same time. However, more of a bug than a feature in my opinion.

This feels unnecessary as it should just cover the following cases

  1. masterVariant was changed but none of the attributes changed and the variant previously existed
  2. masterVariant was changed some attributes changed and the variant existed previsouly
  3. The masterVariant did not exist (must be new product) and all attributes are new

To achieve this and to minimize the amount of needed book keeping in the process I move the masterVariant into the variants array at the first position.

  1. This removes the code duplication
  2. Allowing tracking the identify of the variant (existence essentially) by id, name, etc (sku and key don't seem implemented yet)
  3. Easier checking if the id of the old vs new master variant actually changed

Todo

  • Tests
    • Unit

@codecov
Copy link

codecov bot commented Sep 14, 2017

Codecov Report

Merging #304 into master will increase coverage by 0.3%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master     #304     +/-   ##
=========================================
+ Coverage   98.11%   98.42%   +0.3%     
=========================================
  Files          74       74             
  Lines        1647     1652      +5     
  Branches      363      366      +3     
=========================================
+ Hits         1616     1626     +10     
+ Misses         25       23      -2     
+ Partials        6        3      -3
Impacted Files Coverage Δ
...nc-actions/src/utils/create-build-array-actions.js 100% <100%> (ø) ⬆️
packages/sync-actions/src/products.js 100% <100%> (ø) ⬆️
...ges/sync-actions/src/utils/create-build-actions.js 87.5% <100%> (+4.16%) ⬆️
packages/sync-actions/src/product-actions.js 95.1% <100%> (-0.18%) ⬇️
.../sync-actions/src/utils/create-map-action-group.js 100% <0%> (+71.42%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ec0ad42...f8a6616. Read the comment docs.

Copy link
Contributor Author

@tdeekens tdeekens left a comment

Choose a reason for hiding this comment

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

Just some explanatory comments.

id: objectToRemove.id,
}),
[REMOVE_ACTIONS]: ({ id }, index) => {
// The master variant can not be removed
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't remove the master variant. This was done before by treating it as in the masterVariant property which would non run through the array detection process.

)
actions = actions.concat(mActions)
}
const { variants } = diff
Copy link
Contributor Author

Choose a reason for hiding this comment

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

...all this duplication of handling changes goes away.

.concat(addPriceActions)
}

export function generateChangeMasterVariantAction (oldObj, newObj) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Specific handler to create the action.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to maintain the consistency with previous similar methods by having something like actionsMapMasterVariant or so

const extractMasterVariantId = (fromObj) => {
const variants = Array.isArray(fromObj.variants) ? fromObj.variants : []

return variants[0] ? variants[0].id : undefined
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This feels a bit wonky but eases the code further down. Generally caused by variants not having to be set and hence the id not necessarily having to be present.

}
}

function moveMasterVariantsIntoVariants (before, now) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the hook we pass in to process data before it hits the differ.


// diff 'em
const diffed = diff(before, now)
const [
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function feels generally flaky to me. It shuffles the arguments of new and before around quite a bit and I wonder if that's the intention or actually a bug which we then work around when consuming the diff.

proccedBefore, processedNow,
] = applyOnBeforeDiff(before, now, onBeforeDiff)

const diffed = differ(proccedBefore, processedNow)
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 renamed the arg. The library seems to call a diff the same as the differ creating a diff which confused me quite a bit with this argument shuffling.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just a nitpick: maybe you should rename the variable to processedBefore, and also on line 21

// When adding a new element you don't need the oldObj
config[ADD_ACTIONS](newObj[key][index]),
if (config[ADD_ACTIONS] && isCreateAction(arrayDelta, index)) {
const actionGenerator = config[ADD_ACTIONS]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just general simplications in naming things here. The addition is to pass the index to the callback so it has the information.

id: '123',
masterVariant: {
sku: 'v1', key: 'v2', attributes: [{ name: 'foo', value: 'new value' }],
id: 1, sku: 'v1', key: 'v2', attributes: [{ name: 'foo', value: 'new value' }],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test below felt like it didn't want to test for the creation of the masterVariant. However if there is no id it should be created.

],
}
describe('without master variant in `now`', () => {
describe('with master variant in `before`', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tries to BDD down the tests

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I think the test description is not explanatory enough

A sample below

Actions › without master variant in `now` › with master variant in `before` › should not generate update action to remove master variant

compared to something like this

Actions › should build `addVariant` action

No hard opinion on this though. Usable for me

@tdeekens
Copy link
Contributor Author

The test pass. Is the yarn it failing due to me?

screen shot 2017-09-14 at 12 20 30

Copy link
Contributor

@wizzy25 wizzy25 left a comment

Choose a reason for hiding this comment

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

Thanks for this. feels a bit hacky but looks good nonetheless. I dropped some comments

And the failing build is a build issue. I will push a commit to this branch to fix it

const oldMasterVariantId = extractMasterVariantId(oldObj)

// Previosuly no master variant existed
if (!oldMasterVariantId && newMasterVariantId)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this part; there will always be a [previous] master variant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I wasn't 100% about a. the cases here and b. how to guard against unexpected things. Moreover, the tests had cases where no masterVariant in before existed if I am not mistaken. I guess we could

  1. Both have an id and in now the id changed?
  2. Only now has an id (as else if-branch)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

However, then the loads of tests fail and I assume it's due some "flawed" fixture data maybe.

Copy link
Contributor

@wizzy25 wizzy25 Sep 14, 2017

Choose a reason for hiding this comment

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

I went through the code again and I think I like your implementation as it is.

PS. You should also bump the test coverage 😉

Copy link
Contributor Author

@tdeekens tdeekens Sep 14, 2017

Choose a reason for hiding this comment

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

Voila, a completely untested module now tested. I am begging for the % to increase enough. It was the only "red" one in the coverage report e1a5d56

proccedBefore, processedNow,
] = applyOnBeforeDiff(before, now, onBeforeDiff)

const diffed = differ(proccedBefore, processedNow)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a nitpick: maybe you should rename the variable to processedBefore, and also on line 21

wizzy25 and others added 2 commits September 14, 2017 12:58
remove package-lock file in int tests because we
don't want to cache the dependencies as we always

want to test the latest versions
@tdeekens
Copy link
Contributor Author

I verified with the platform that a masterVariant will always be auto generated. If you create a project without any variants a masterVariant will be implicitly added. As a result all test cases with e.g.

masterVariant: {
  //No id
  name: 'foo',
  prices: {},
}

are "flawed" as they won't happen. The now and its masterVariant you diff will always have an id. The id is just a sequential value which can't be changed afterwards.

@tdeekens tdeekens force-pushed the feature/change-master-variant-action branch from ac4b76a to 5c6c66e Compare September 14, 2017 15:46
@tdeekens
Copy link
Contributor Author

tdeekens commented Sep 14, 2017

So from my side this is ready. To be released either on the @latest or @next tag. We can have it on @next first maybe, keep this open and test it in a branch in the MC?

const oldMasterVariantId = extractMasterVariantId(oldObj)

// Previosuly no master variant existed
if (!oldMasterVariantId && newMasterVariantId)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Referring to the comment above and your statement too: this won't happen due to the workings of our API and the implicit generation of a masterVariant.


expect(actions).toEqual([
{ action: 'addVariant', attributes: [{ name: 'foo', value: 'bar' }], id: 2, sku: 'v1' },
{ action: 'changeMasterVariant', variantId: 2 },
Copy link

Choose a reason for hiding this comment

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

It should also delete the variant with id 1, shouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is one of the semantics I wasn't 100% sure about. It felt safer not to remove than to just throw it away. I'll see what other reviewers say.

Copy link

Choose a reason for hiding this comment

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

if the goal is that now = before + all update actions, then we have to remove the variant. Otherwise we have one variant too much in now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeap. You're right. I am thinking how we can bake it in.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeap, I agree with us removing the update actions.


expect(actions).toEqual([
{ action: 'changeMasterVariant', variantId: 1 },
{ action: 'setAttribute', name: 'foo-2', value: 'bar-3', variantId: 2 },
Copy link

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@hisabimbola hisabimbola left a comment

Choose a reason for hiding this comment

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

Thanks for the refactor, I added minor comments that I think we can discuss about.

if (newMasterVariantId && oldMasterVariantId !== newMasterVariantId)
return createChangeMasterVariantAction(newMasterVariantId)

return null
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can make this more consistent by returning [] if no actions is generated

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can I didn't as it will and should always just return one action so the array there felt odd but I'll change it for consistency.

.concat(addPriceActions)
}

export function generateChangeMasterVariantAction (oldObj, newObj) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to maintain the consistency with previous similar methods by having something like actionsMapMasterVariant or so

allActions.push(mapActionGroup('variants', () =>
productActions.actionsMapVariants(diff, oldObj, newObj)))

const changeMasterVariantAction =
Copy link
Contributor

Choose a reason for hiding this comment

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

Buidling from previous comment, if we return empty array here, there will not be any need to check if value is present


return [
hasMasterVariant(before) ? move(before) : before,
hasMasterVariant(now) ? move(now) : now,
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure how often we will have this scenario but what if the masterVariant object is not passed in the now object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what can not happen in theory. Whenever a project has been created implicitly a master variant will be added. Theoretically we could remove these checks but our test data is sometimes a bit messy. E.g. not having a masterVariant in now of before.

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean from the user end, I think what you describing is the before(what already exists in the CTP)

If we understand each other though 🤔

Copy link
Contributor Author

@tdeekens tdeekens Sep 15, 2017

Choose a reason for hiding this comment

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

Hm, so I think there are a couple of invariants in this discussions

  1. The before can be assumed to always have a master variant
  2. The now should also always have a master variant
    2.1 If it doesn't we can assume that the first variant ought to be the new master variant

If non the the cases hold be should either throw or do nothing which breaks things. By not breaking we can "hope" that the before (if without master variant) that the now also doesn't have one. Cause then we will not change the master variant. Which is non destructive in that sense.

],
}
describe('without master variant in `now`', () => {
describe('with master variant in `before`', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I think the test description is not explanatory enough

A sample below

Actions › without master variant in `now` › with master variant in `before` › should not generate update action to remove master variant

compared to something like this

Actions › should build `addVariant` action

No hard opinion on this though. Usable for me

variants: [],
}

it('should not generate update action to remove master variant',
Copy link
Contributor

Choose a reason for hiding this comment

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

Building from comment above, this test fails

describe('no mastervariant in `now` obj', () => {
  const before = {
    id: '123',
    version: 1,
    masterVariant: {
      id: 1,
      sku: 'v1',
      attributes: [{ name: 'foo', value: 'bar' }],
    },
    variants: [],
  }

  const now = {
    id: '123',
    // <-- no masterVariant
    variants: [
      {
        id: 2,
        sku: 'v1',
        attributes: [{ name: 'foo', value: 'bar' }],
      },
    ],
  }

  it('should generate a addVariant action', () => {
    const actions = productsSync.buildActions(now, before)
    expect(actions).toEqual([
      { action: 'addVariant', sku: 'v1', attributes: [{ name: 'foo', value: 'bar' }] },
    ])
  })
})

Copy link
Contributor Author

@tdeekens tdeekens Sep 15, 2017

Choose a reason for hiding this comment

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

Yeah, cause that test lives in the grey zone mentioned above. What do we assume to be intended here? You had a master variant and now you don't want one. Which basically is breaking with our invariants.

Moreover, the test fails as the id 1 doesn't exist anymore.

  1. So should it be removed? (1)?
  2. Should 2 be implicitly be the new master?
    Did use user intend to do that?
  3. Should we keep both?

I think with now you declare where you want to end up at. This test is flawed cause the state the user wants to end up in is not valid and we can only assume what was meant.


expect(actions).toEqual([
{ action: 'addVariant', attributes: [{ name: 'foo', value: 'bar' }], id: 2, sku: 'v1' },
{ action: 'changeMasterVariant', variantId: 2 },
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeap, I agree with us removing the update actions.

id: '123',
version: 1,
masterVariant: {
id: 2,
Copy link
Contributor

Choose a reason for hiding this comment

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

masterVariant cannot have a variantId of 2. I think it's always 1

Cc @yanns

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can have an id of 2 otherwise you'd never be able to move the master variant. The id is immutable and sequentially assigned.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, okay, good to know that variantId does not change.

Copy link

Choose a reason for hiding this comment

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

I confirm when @tdeekens says.
Master variant id is not always 1. The variant id is the invariant 🤣

@@ -0,0 +1,89 @@
import createMapActionGroup from '../../src/utils/create-map-action-group'

describe('', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure why this is empty string.

using createMapActionGroup might work since that's the method we are testing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ups.

@hisabimbola
Copy link
Contributor

I also think we should add more comments in the code to explain a couple of decisions and why.

I find the sync-actions a bit hard to read/understand, and mostly because of the custom rules of the jsondiffpatch and lack of comments explaining irregular decisions done in the code.

@tdeekens
Copy link
Contributor Author

I find the sync-actions a bit hard to read/understand, and mostly because of the custom rules of the jsondiffpatch

You should have seen my face yesterday. I got a bold spot on my head from scratching 😄

@tdeekens
Copy link
Contributor Author

tdeekens commented Sep 15, 2017

Regarding the BDD on a comment above I can't comment on

Actions › without master variant in `now` › with master variant in `before` › should not generate update action to remove master variant

This gives context as in Given, When Then. Which I think is nicer to follow along as it sets up the context towards what the then is. Otherwise you have to peel the Given and When off the data which needs more knowledge.

@tdeekens
Copy link
Contributor Author

@hisabimbola I went over things a little. We now remove the variant if if doesn't exist anymore in variants. I also changed the test structure a bit to hopefully be nicer to read along.

@tdeekens
Copy link
Contributor Author

After talking to @hisabimbola we decided not to have validation on this PR. Changes it might be a breaking change to loads of consumers of the library. Furthermore, our test data does not always contain all required fields.

This is ready then for a last round of review.

@hisabimbola you want to publish it on the @latest or @next tag?


const changeMasterVariantActions =
productActions.actionsMapMasterVariant(oldObj, newObj)
if (changeMasterVariantActions.length > 0)
Copy link
Contributor

@hisabimbola hisabimbola Sep 18, 2017

Choose a reason for hiding this comment

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

I think we don't need the condition again, if it's empty array, it's fine for us to push it. it gets flattened out anyways below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah ok. Thought better to be safe than sorry. Here you go f8a6616

Copy link
Contributor

@hisabimbola hisabimbola left a comment

Choose a reason for hiding this comment

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

This looks good now

Thanks for helping out @tdeekens 🚀 💯 🥇

The releases are done automatically base on the commit message. More info here

Let me know if you need help with that

:shipit:

@tdeekens tdeekens merged commit 81df90b into master Sep 18, 2017
@tdeekens tdeekens deleted the feature/change-master-variant-action branch September 18, 2017 09:36
@wizzy25 wizzy25 mentioned this pull request Sep 18, 2017
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants