Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor API code for GitHub backend #508

Closed
wants to merge 16 commits into from
Closed

Conversation

Benaiah
Copy link
Contributor

@Benaiah Benaiah commented Aug 2, 2017

- Summary

Refactor of API.js in the GitHub backend. General improvements to
code style, removal of mutation, function interdependencies, and race
conditions (see the commit message for 732ede1 for more details on
that). Code modified is generally much shorter.

Other refactors I'm working on for this PR:

  • Refactor updateTree
  • Refactor editorialWorkflowGit
  • Replace _ with individual function imports.
  • Use Immutable objects throughout to avoid having to convert to and from them in individual functions.
  • Refine request handling (possibly remove meta field from requesterrors if a better solution is found).

EDIT: I'm closing the scope of this so we can get it merged. Further refactors can come in later PRs.

- Test plan

Tested manually, and existing tests pass.

- Description for the changelog

  • Refactor API.js in GitHub backend

- A picture of a cute animal (not mandatory but encouraged)

image

Copy link
Contributor

@tortilaman tortilaman left a comment

Choose a reason for hiding this comment

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

Great optimizations and modernizations overall. Just curious about use of ternary operators to supply empty arrays & objects as opposed to using an && truthiness check. However you describe those.

...headers,
...(this.token ? { Authorization: `token ${ this.token }` } : {}),
Copy link
Contributor

@tortilaman tortilaman Aug 2, 2017

Choose a reason for hiding this comment

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

is there a reason to include the empty {} when there's no token as opposed to using

...(this.token && { Authorization: `token ${ this.token}` }),

I would think unless something is looking for data based on array position, removing an empty object would be preferable.

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 don't think this will work, since ...(undefined) and ...(false) both throw a SymbolError.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, ok.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick/point of discussion: I honestly think this is less readable. Prettier and more succinct, but less readable. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I take it back, this whole thing is a massive improvement. Going to abstain from nitpicking in favor of faster 🚢ping.

})
return this.request('/user/repos').then(repos =>
Object.keys(repos).some(
key => (repos[key].full_name === this.repo) && repos[key].permissions.push
Copy link
Contributor

Choose a reason for hiding this comment

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

Ooh. Wasn't aware of some. Very useful stuff.

const contentType = response.headers.get("Content-Type");
if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response);
return Promise.all([response, response.json()]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome moving to promises!

Benaiah added 2 commits August 2, 2017 12:25
- Removes `parseJsonResponse`
- Errors on non-ok responses for text responses as well as JSON
  responses
- Adds an optional `meta` field to `APIError` in
  `src/valueObjects/errors/APIError.js`
- Passes github error responses to `APIErrors` generated by `request`
  in `API.js` of github backend
@Benaiah
Copy link
Contributor Author

Benaiah commented Aug 2, 2017

Turns out I'd broken storeMetadata - it's now fixed. I've also added a big refactor of the updateTree function (@tortilaman, if you could review that change as well I'd appreciate it).

@tech4him1
Copy link
Contributor

tech4him1 commented Aug 2, 2017

Looks like Travis tests are failing on 8872d18:

github API › should create PR with correct base branch name when publishing with editorial workflow

Expected received Promise to resolve, instead it rejected to value [TypeError: Object.entries is not a function]

@Benaiah
Copy link
Contributor Author

Benaiah commented Aug 2, 2017

@tech4him1 thanks - it was working on my machine, so I foolishly assumed it was being polyfilled. Replacing with entries from lodash.

Copy link
Contributor

@tortilaman tortilaman left a comment

Choose a reason for hiding this comment

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

I've never looked into Lodash before, but going through this I'm definitely going to have to use this in the future. Super useful for working with data.

];

const newItems = Object.entries(fileTree).filter(([filename]) => !added[filename]);
const [newFiles, newDirs] = partition(newItems, ([, file]) => this.isFile(file));
Copy link
Contributor

@tortilaman tortilaman Aug 2, 2017

Choose a reason for hiding this comment

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

Not following the ([, file[) part of this. Other than that this seems like a really nice improvement.

Copy link
Contributor Author

@Benaiah Benaiah Aug 2, 2017

Choose a reason for hiding this comment

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

@tortilaman it's destructuring the argument and ignoring the first element. For example:

// these are equivalent

const f1 = ([ x, y ]) => y > 0
const f2 = ([, y]) => y > 0

f1(0, 1) // true
f2(0, 1) // true
f1(1, 0) // false
f2(1, 0) // false

// you can use multiple commas

const fourthElement = ([,,,el]) => el
f3([1, 2, 3, 4]) // 4

It allows us to prevent creating a name for something we never use later on. If it's obscure enough it's confusing to most people I can change it.

@Benaiah
Copy link
Contributor Author

Benaiah commented Aug 2, 2017

@tortilaman Got another big function refactored - editorialWorkflowGit. Lodash is indeed super useful (though I've been playing with Ramda lately and that feels like the next level).

@Benaiah Benaiah changed the title (WIP) Refactor API code for GitHub backend Refactor API code for GitHub backend Aug 3, 2017
@Benaiah Benaiah requested a review from erquhart August 3, 2017 02:42
tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Aug 5, 2017
@Benaiah
Copy link
Contributor Author

Benaiah commented Aug 7, 2017

Fixed a bug in my updateTree refactor which I uncovered while writing my upcoming GitHub API tests.

tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Aug 8, 2017
@tech4him1 tech4him1 mentioned this pull request Aug 8, 2017
10 tasks
tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Aug 11, 2017
tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Aug 14, 2017
tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Aug 14, 2017
@kalinchernev
Copy link
Contributor

I was about to create an issue about several issues related to the API calls to github, but found this pull request. I didn't see any modifications in the tests, so it wasn't very easy to figure if these will be related to any changes already fixed.

I suppose many fixes might be already in via the request refactoring, anyway please test these.

  1. Collection folder setting can break the API request silently

For example, I had a config like this:

collections:
    folder: "content/sample-posts/"

The console yields an error because the following request fails api.github.com/repos/kalinchernev/gatsby-jammin/contents/content/sample-posts//15-08-2017/test.md?ts=1502817755076&ref=master - it has double slash.

This configuration fixes this issue

collections:
    folder: "content/sample-posts"

Possible improvement here could be that at least there is a simple component in the body that gives feedback to the user there is an internal error. And of course having some checks for this config entry to ensure correct request to the github API.

  1. The list of posts (or whatever collection type) does not show existing items

I'm using Gatsby, and structure is

.
├── 2017-08-13-test.md
└── sample-posts
    ├── 01-01-2017
    │   └── index.md
    ├── 01-02-2017
    │   └── index.md
    ├── 01-03-2017
    │   └── index.md
    ├── 01-07-2017-Big_Sample_Post
    │   └── index.md
    ├── 02-03-2017
    │   └── index.md
    ├── 02-04-2017
    │   └── index.md
    ├── 15-08-2017
    │   └── test2.md
    ├── 17-03-2017
    │   └── index.md
    └── 23-03-2017
        └── index.md

None of these are displayed in the list of content, even when content has been created via the workflow.

Repo is here if that might help.

@Benaiah
Copy link
Contributor Author

Benaiah commented Aug 15, 2017

@kalinchernev the first issue is definitely related to how we construct requests - we should be checking to avoid doubling slashes at the beginning and end of user-supplied strings. The other problem appears to be due to the fact that each of your entries is in its own subfolder - this isn't currently supported by the CMS. You're welcome to make an issue regarding that if you want to discuss it further, but it's a bigger concern than those addressed by this PR, which is mostly focused on code cleanup.

@erquhart
Copy link
Contributor

@kalinchernev see #513 for the related improvement.

@kalinchernev
Copy link
Contributor

@erquhart thanks, I didn't open an issue as suggested, as I managed by flattening as discussed in the existing issue :)

Copy link
Contributor

@erquhart erquhart left a comment

Choose a reason for hiding this comment

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

@Benaiah solid work on this, thank you!

One place that we still need to improve is not just readability, but understandability, or clarity. Although this code is much leaner and far more self documenting, it's still very dense. We need to break stuff up and notate.

Because of said density, my review only went so deep, so it would be nice to merge this together with your testing PR. That said, LGTM.

...headers,
...(this.token ? { Authorization: `token ${ this.token }` } : {}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick/point of discussion: I honestly think this is less readable. Prettier and more succinct, but less readable. Thoughts?

return baseHeader;
}

parseJsonResponse(response) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice fat trimmin'

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: the Git Gateway backend is now using this as well - maybe worth keeping and refactoring.

...headers,
...(this.token ? { Authorization: `token ${ this.token }` } : {}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I take it back, this whole thing is a massive improvement. Going to abstain from nitpicking in favor of faster 🚢ping.

return files
.map(file => ({ ...file, file: true }))
.reduce((tree, file) => tree.setIn(file.path.split("/"), file), Map())
.toJS();
Copy link
Contributor

Choose a reason for hiding this comment

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

😍

Choose a reason for hiding this comment

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

Does it matter that the original version did not include nested keys that were empty and the new one does?
I.e. .filter(part => part) appears to have been dropped.

});
return Promise.all([fileTreePromise, this.getBranch()])
.then(([fileTree, branchData]) => this.updateTree(branchData.commit.sha, "/", fileTree))
.then(changeTree => this.commit(options.commitMessage, changeTree));
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for patch?

Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering that too, was it moved to a different location?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm assuming it was superfluous, but wanted to confirm.

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 don't remember removing this intentionally, so it was likely an oversight. Thanks for the catch!

timeStamp: new Date().toISOString(),
};

return (unpublished
Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of this really should be broken down and notated for clarity at some point in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely agreed.

Functions refactored:

 - `storeMetadata`
 - `composeFileTree`
 - `persistFiles`
 - `uploadBlob`

These refactors had to be done together, since each the methods
refactored had implicit dependencies on mutations made by the others.
Specifically, `composeFileTree` and `uploadBlob` had to be called in a
specific order to mutate their arguments correctly. This led to a race
condition in `persistFiles` that was (fortunately) only affecting
debugging.

This commit removes the mutation and makes the functions more
independent, in addition to making general readability and code length
improvements to the affected functions. `composeFileTree` has also had
its filtering of uploaded files removed, as that was a concern better
handled by the caller.
cbarrett pushed a commit to cbarrett/netlify-cms that referenced this pull request Oct 13, 2017
Copy link
Contributor

@erquhart erquhart left a comment

Choose a reason for hiding this comment

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

@Benaiah I know this PR is on the back burner atm, but I think we're going to need a lot of source documentation and tests added before we can safely merge. Two reasons:

  1. Succinctness and immutability are huge code improvements, but we can't risk bugs for them (hence the need for tests).

  2. Things are a little easier to read here, but not by much. It isn't a fault in your refactor, it's just that there's too much going on here to rely on self-documenting. Source docs can resolve this, and will also make reviews of this PR much more informed.

cbarrett pushed a commit to cbarrett/netlify-cms that referenced this pull request Nov 21, 2017
Copy link
Contributor

@erquhart erquhart left a comment

Choose a reason for hiding this comment

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

@Benaiah if you can address the comments, get this rebased, and we get one more approval, we can merge.

return baseHeader;
}

parseJsonResponse(response) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Update: the Git Gateway backend is now using this as well - maybe worth keeping and refactoring.

return Promise.all(updates)
.then(updates => this.request(`${ this.repoURL }/git/trees`, {
.then(resolvedUpdates => this.request(`${ this.repoURL }/git/trees`, {
method: "POST",
body: JSON.stringify({ base_tree: sha, tree: updates }),
Copy link
Contributor

Choose a reason for hiding this comment

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

This still needs to be fixed.

trongthanh pushed a commit to goalifyplus/netlify-cms that referenced this pull request Jan 4, 2018
erquhart pushed a commit to tech4him1/netlify-cms that referenced this pull request Jan 4, 2018
tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Jan 4, 2018
})
.catch((error) => {
throw new APIError(error.message, responseStatus, 'GitHub');
.catch(err => [err, null])
Copy link
Contributor

Choose a reason for hiding this comment

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

@Benaiah You mentioned that:

This should be .catch(err => Promise.reject([err, null)) so the other .catch block catches and destructures it properly. This is my bad - it's the same way in my GH API refactor PR.

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 correct, thanks!

? Object.entries(options.params).map(
([key, val]) => `${ key }=${ encodeURIComponent(val) }`)
: [];
return `${ this.api_root }${ path }?${ [cacheBuster, ...encodedParams].join("&") }`;

Choose a reason for hiding this comment

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

Can we make use of modern API here (to avoid re-inventing the wheel)?
Specifically:

  • URLSearchParams for the query string
  • URL for the merging of parts (root, path and query string)

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 note: URLSearchParams isn't supported in MS Edge <17. Otherwise, I think this is a great idea.

Benaiah pushed a commit to Benaiah/netlify-cms that referenced this pull request Apr 5, 2018
Benaiah pushed a commit that referenced this pull request Apr 19, 2018
Benaiah pushed a commit that referenced this pull request Apr 20, 2018
Benaiah pushed a commit that referenced this pull request May 4, 2018
Benaiah pushed a commit that referenced this pull request May 7, 2018
Benaiah pushed a commit that referenced this pull request May 18, 2018
Benaiah pushed a commit that referenced this pull request May 18, 2018
Benaiah pushed a commit that referenced this pull request May 22, 2018
Benaiah pushed a commit that referenced this pull request May 25, 2018
Benaiah pushed a commit that referenced this pull request May 30, 2018
erquhart pushed a commit to erquhart/netlify-cms that referenced this pull request Jun 4, 2018
Benaiah pushed a commit to Benaiah/netlify-cms that referenced this pull request Jun 8, 2018
tech4him1 added a commit to tech4him1/netlify-cms that referenced this pull request Jul 30, 2018
@erquhart
Copy link
Contributor

Closing as stale, leaving branch deletion for @Benaiah.

@erquhart erquhart closed this Jul 31, 2018
@erezrokah erezrokah deleted the gh-api-refactors branch February 7, 2021 15:37
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.

8 participants