-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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
GitLab backend built with cursor API #1343
Conversation
Deploy preview for netlify-cms-www ready! Built with commit d86c610 |
src/valueObjects/Cursor.js
Outdated
@@ -0,0 +1,52 @@ | |||
import isArray from "lodash/isArray"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this file be moved/renamed? Cursor
is not actually a class, just a definition and validation function for a specified set of properties on any object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to provide a Cursor factory?
src/reducers/cursors.js
Outdated
// Since pagination can be used for a variety of views (collections | ||
// and searches are the most common examples), we namespace cursors by | ||
// their type before storing them in the state. | ||
export const collectionEntriesCursorKey = collectionName => `collectionEntries.${ collectionName }`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows us to use pagination only for collections at the moment, while still using a Redux structure that will support other uses of cursors.
src/reducers/search.js
Outdated
@@ -38,7 +38,7 @@ const entries = (state = defaultState, action) => { | |||
map.set('isFetching', false); | |||
map.set('page', page); | |||
map.set('term', searchTerm); | |||
map.set('entryIds', page === 0 ? entryIds : map.get('entryIds', List()).concat(entryIds)); | |||
map.set('entryIds', (!page || page === 0) ? entryIds : map.get('entryIds', List()).concat(entryIds)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we're using cursor-based pagination, we do not want the entryIds
to be appended to, since we only model knowledge of one set of entries and one cursor, which we retrieve from the backend.
src/lib/promiseHelper.js
Outdated
@@ -18,3 +18,5 @@ export const resolvePromiseProperties = (obj) => { | |||
// resolved values | |||
Object.assign({}, obj, zipObject(promiseKeys, resolvedPromises))); | |||
}; | |||
|
|||
export const then = func => p => ((p && p.then) ? p.then(func) : func(p)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very simple function that lets you do something like the following (flow
being from lodash):
const fn = flow([
then(x => x + 2), // this will work whether or not x is a Promise
then(x => x * 2),
])
fn(2) // -> 8
fn(Promise.resolve(2)) // -> Promise resolving to 8
One notable question: should this return synchronously if it doesn't receive a Promise (as it does currently), or should it be rewritten as:
export const then = func => p => Promise.resolve(p).then(func)
const fn = flow([
then(x => x + 2), // this will work whether or not x is a Promise
then(x => x * 2),
])
fn(2) // -> Promise resolving to 8
fn(Promise.resolve(2)) // -> Promise resolving to 8
The advantage of this would be that then
would always return a Promise, regardless of what was passed in, which seems more in line with the name and analogy to the .then
method it's wrapping.
// returns all the collected entries. Used to retrieve all entries | ||
// for local searches and queries. | ||
async listAllEntries(collection) { | ||
if (collection.get("folder") && this.implementation.allEntriesByFolder) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not very happy with how this check turned out, but it's basically necessary due to the use of two methods for listing collection (entriesByFiles
and entriesByFolder
). Refactoring the backend API should make this much neater by unifying the two.
Note that test-repo
does not have a special implementation of allEntriesByFolder
- it simply returns a cursor, and listing all entries can be done using just the cursor (as long as it supports the "next"
action). GitLab has a particular implementation of this for performance reasons (it raises the files-per-page to GitLab's maximum, and doesn't list the entries in reverse).
src/actions/search.js
Outdated
@@ -1,10 +1,6 @@ | |||
import fuzzy from 'fuzzy'; | |||
import { currentBackend } from 'Backends/backend'; | |||
import { getIntegrationProvider } from 'Integrations'; | |||
import { selectIntegration, selectEntries } from 'Reducers'; | |||
import { selectInferedField } from 'Reducers/collections'; | |||
import { WAIT_UNTIL_ACTION } from 'Redux/middleware/waitUntilAction'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: remove this import, as well as fuzzy
, which are not used in the file.
be2aeed
to
61fef08
Compare
Deploy preview for cms-demo ready! Built with commit d86c610 |
cb0c93e
to
d7bedc9
Compare
Update: implicit grant authentication support (#1314) has been added to this branch. |
Built current changes and tested local, production. Media, post update, creation and delete working. @Benaiah let me know if there is something specific we need to test. Great work! |
ac0b0da
to
a35dd3d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work @Benaiah. My comments are all around consolidation of Cursor logic so cursor interactions are discreet across the codebase, as we discussed.
src/actions/entries.js
Outdated
// structure. | ||
.then(response => ((!response.cursor) || validateCursor(response.cursor) | ||
? response | ||
: Promise.reject(invalidCursorError(response.cursor)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: multiple negatives make this conditional a bit confusing to read, consider:
(response.cursor && !validateCursor(response.cursor)) ? Promise.reject(..) : response
If there's no cursor or if the cursor is not valid
versus
If there's a cursor and the cursor is not valid
getCursorActionFunctions = (cursor, handler) => cursor && cursor.actions | ||
? cursor.actions.reduce((acc, action) => ({ ...acc, [action]: handler(action) }), {}) | ||
: {}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor interaction functions like the ones added to this file should be moved to the Cursor.js
library and imported.
@@ -62,11 +78,15 @@ function mapStateToProps(state, ownProps) { | |||
const entries = selectEntries(state, collection.get('name')); | |||
const isFetching = state.entries.getIn(['pages', collection.get('name'), 'isFetching'], false); | |||
|
|||
return { publicFolder, collection, page, entries, isFetching, viewStyle }; | |||
const rawCursor = state.cursors.get(collectionEntriesCursorKey(collection.get('name'))); | |||
const cursor = rawCursor && pick(rawCursor, ["meta", "actions"]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic could probably be moved to a selector in the Cursor reducer.
be8e092
to
c125416
Compare
c125416
to
cce0e3c
Compare
Very excited to see this PR move forward to merged, so the BitBucket work can flow on from there. When Netlify supports all three major VCS for the CMS side, it will be a superb day for all. Keep up the great work! |
ed05f5f
to
f7fb1dc
Compare
This PR is now ready for review! Notable changes for those who have already looked at the code prior:
|
7e05ec3
to
40438e8
Compare
This PR has been updated with preliminary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a checkbox to the task list to add docs before this gets merged. I'm happy to make the necessary updates and PR them against this branch, but I haven't had a chance to test the GitLab backend myself, so I need to figure out if there's anything different in implementation. (@erquhart has offered to look into this.)
I'm also checking into doc updtes for git-gateway, both at the repo level and in Netlify's docs.
if (authType === "implicit") { | ||
this.auth = new ImplicitAuthenticator({ | ||
auth_url: this.props.config.getIn(['backend', 'auth_url'], 'https://gitlab.com/oauth/authorize'), | ||
appid: this.props.config.getIn(['backend', 'appid']), |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
|
throw new Error("The GitLab backend does not support the Editorial Workflow.") | ||
} | ||
|
||
if (!options.proxied && config.getIn(["backend", "repo"]) == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be this.options.proxied
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point - options
should also have a default value of {}
(or L17 should do a null check before using the spread operator).
Docs PR, against this branch: #1413 |
src/lib/implicit-oauth.js
Outdated
this.auth_url = config.auth_url; | ||
const baseURL = trimEnd(config.base_url, '/'); | ||
const authEndpoint = trim(config.auth_endpoint, '/'); | ||
this.auth_url = `${ config.base_url }/${ config.auth_endpoint }`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo here -- we want to use the trimmed values, not the ones from the config.
844912f
to
d86c610
Compare
Through some testing today, we did run into a scenario with a protected branch. The user made a change and published, it stated that the publish was successful but nothing happened. Upon further review it was discovered the branch was protected and the user didn't have permission to push. This error should probably be surfaced to the user. |
This PR contains the commits from #517, rebased onto the cursor API. This will now be the primary development PR for GitLab.
The best summary of the changes is the commit log, but a quick list of the most major aspects are here. (Once this PR has been created, I'll be doing much more detailed code review comments to request input on specific questions):
backend.js
(except for media library integrations).unsentRequest.js
, and now uses cursor-based pagination.backend.js
method has been added to retrieve all entries of a collection regardless of its pagination, for use in local search and querying.Todo before merge:
example/config.yml
, remove remaining debug codeunsentRequest
.1Todo after merge (some of these are not GitLab-specific, but general backend work):
backend.js
.listAllEntries
). Currently I have a smart git cache working, but clunky - I'm doing some work to make it asynchronous and support partial knowledge of objects (for instance, truncated trees).1 This could also be done by making integration pagination use cursors, but that may take longer than the simple fix.
2 These steps may end up being done after merge, depending on how we decide to release this.