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

GitLab backend built with cursor API #1343

Merged
merged 45 commits into from
Jun 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6162421
Create cursor definition and Redux structure
Benaiah Apr 5, 2018
f5d54fe
Create backwards-compatible cursor symbol for backend interface
Benaiah Apr 9, 2018
e3ba975
Make test-repo backend use cursor API
Benaiah Apr 19, 2018
f93ab1c
Infinite pagination for cursors
Benaiah May 17, 2018
0eefd9b
Add an optional `meta` field to `APIError` in `src/valueObjects/error…
Benaiah Jul 28, 2017
0179113
Initial GitLab commit.
tech4him1 Aug 4, 2017
3c984da
Fix authentication/permission checking.
tech4him1 Aug 5, 2017
53db61c
Make images with the same filename as already existing images overwrite
tech4him1 Aug 7, 2017
503b617
Remove unneeded dependencies.
tech4him1 Aug 8, 2017
7ad5814
Remove old GitHub code.
tech4him1 Aug 8, 2017
a93ad46
Code cleanup from #508.
tech4him1 Aug 11, 2017
71258a5
Clarify comment.
tech4him1 Aug 14, 2017
e4764fb
Fix permission checking for GitLab projects that are in groups/subgro…
tech4him1 Aug 16, 2017
6c22e2e
Cleanup `request` function and other minor cleanup.
tech4him1 Aug 30, 2017
98ea3e4
migrate GitLab backend to 1.0
erquhart Jan 4, 2018
f686a04
Download GitLab files as raw
tech4him1 Jan 4, 2018
b56f4c3
add restoreUser method to GitLab backend
erquhart Jan 4, 2018
b5da72e
update hasWriteAccess and logout for GitLab
tech4him1 Jan 4, 2018
b2f287c
add media library support for GitLab
tech4him1 Jan 4, 2018
b348a32
fix gitlab collection caching
erquhart Jan 4, 2018
0d3092d
remove old entry-plus-media persist logic
erquhart Jan 5, 2018
a5c3e82
Fix access for GitLab group-owned repos.
tech4him1 Jan 8, 2018
a13f179
simplify persistFiles
tech4him1 Jan 10, 2018
9239cac
Fix auth scope
Benaiah Apr 19, 2018
d2bb871
Implement cursor API for GitLab
Benaiah Apr 19, 2018
086eb69
Refactor GitLab backend with unsentRequest and reverse pagination
Benaiah Apr 25, 2018
8e63428
Implement search for GitLab
Benaiah May 3, 2018
05c2f79
Add implicit OAuth for GitLab.
tech4him1 Apr 24, 2018
39af3d6
Fetch all files when listing media from GitLab
Benaiah Jun 5, 2018
50404dd
Remove unnecessary imports in GitLab API
Benaiah Jun 5, 2018
1b68364
Improve the readability of reverseCursor in GitLab API
Benaiah Jun 5, 2018
01d7667
Improve error handling and request parsing in GitLab API
Benaiah Jun 5, 2018
110785f
Automatically call fromURL on string args to unsentRequest functions
Benaiah Jun 5, 2018
3bfe9e3
Filter folders from file-listing functions in GitLab API
Benaiah Jun 5, 2018
bf410a4
print error messages in error boundary component
erquhart Jun 5, 2018
40438e8
Git Gateway: lazy loading and GitLab support
Benaiah Jun 6, 2018
28d6ce5
Rename appid to app_id
Benaiah Jun 7, 2018
1669f29
GitLab: fix collection failure if entry loading fails
Benaiah Jun 7, 2018
0fb3ebf
Update default git-gateway endpoint
Benaiah Jun 8, 2018
c4c403e
Fix instantiation of proxied backend implementation classes
Benaiah Jun 8, 2018
16ebe26
Add missing `auth_endpoint` to GitLab's Netlify auth.
tech4him1 Jun 8, 2018
3678e5d
Remove console.logs
Benaiah Jun 10, 2018
c92e39e
Fix integrations pagination
Benaiah Jun 11, 2018
117e8b1
Change auth_url to base_url+auth_endpoint for GitLab implicit auth
Benaiah Jun 11, 2018
d86c610
Fix querying through integrations
Benaiah Jun 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

var ONE_DAY = 60 * 60 * 24 * 1000;

for (var i=1; i<=10; i++) {
for (var i=1; i<=20; i++) {
var date = new Date();

date.setTime(date.getTime() + ONE_DAY);
Expand Down
99 changes: 93 additions & 6 deletions src/actions/entries.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { List } from 'immutable';
import { fromJS, List, Set } from 'immutable';
import { actions as notifActions } from 'redux-notifications';
import { serializeValues } from 'Lib/serializeEntryValues';
import { currentBackend } from 'Backends/backend';
import { getIntegrationProvider } from 'Integrations';
import { getAsset, selectIntegration } from 'Reducers';
import { selectFields } from 'Reducers/collections';
import { selectCollectionEntriesCursor } from 'Reducers/cursors';
import Cursor from 'ValueObjects/Cursor';
import { createEntry } from 'ValueObjects/Entry';
import ValidationErrorTypes from 'Constants/validationErrorTypes';
import isArray from 'lodash/isArray';

const { notifSend } = notifActions;

Expand Down Expand Up @@ -80,13 +83,15 @@ export function entriesLoading(collection) {
};
}

export function entriesLoaded(collection, entries, pagination) {
export function entriesLoaded(collection, entries, pagination, cursor, append = true) {
return {
type: ENTRIES_SUCCESS,
payload: {
collection: collection.get('name'),
entries,
page: pagination,
cursor: Cursor.create(cursor),
append,
},
};
}
Expand Down Expand Up @@ -238,6 +243,16 @@ export function loadEntry(collection, slug) {
};
}

const appendActions = fromJS({
["append_next"]: { action: "next", append: true },
});

const addAppendActionsToCursor = cursor => Cursor
.create(cursor)
.updateStore("actions", actions => actions.union(
appendActions.filter(v => actions.has(v.get("action"))).keySeq()
));

export function loadEntries(collection, page = 0) {
return (dispatch, getState) => {
if (collection.get('isFetching')) {
Expand All @@ -247,14 +262,86 @@ export function loadEntries(collection, page = 0) {
const backend = currentBackend(state.config);
const integration = selectIntegration(state, collection.get('name'), 'listEntries');
const provider = integration ? getIntegrationProvider(state.integrations, backend.getToken, integration) : backend;
const append = !!(page && !isNaN(page) && page > 0);
dispatch(entriesLoading(collection));
provider.listEntries(collection, page).then(
response => dispatch(entriesLoaded(collection, response.entries.reverse(), response.pagination)),
error => dispatch(entriesFailed(collection, error))
);
provider.listEntries(collection, page)
.then(response => ({
...response,

// The only existing backend using the pagination system is the
// Algolia integration, which is also the only integration used
// to list entries. Thus, this checking for an integration can
// determine whether or not this is using the old integer-based
// pagination API. Other backends will simply store an empty
// cursor, which behaves identically to no cursor at all.
cursor: integration
? Cursor.create({ actions: ["next"], meta: { usingOldPaginationAPI: true }, data: { nextPage: page + 1 } })
: Cursor.create(response.cursor),
}))
.then(response => dispatch(entriesLoaded(
collection,
response.cursor.meta.get('usingOldPaginationAPI')
? response.entries.reverse()
: response.entries,
response.pagination,
addAppendActionsToCursor(response.cursor),
append,
)))
.catch(err => {
dispatch(notifSend({
message: `Failed to load entries: ${ err }`,
kind: 'danger',
dismissAfter: 8000,
}));
return Promise.reject(dispatch(entriesFailed(collection, err)));
});
};
}

function traverseCursor(backend, cursor, action) {
if (!cursor.actions.has(action)) {
throw new Error(`The current cursor does not support the pagination action "${ action }".`);
}
return backend.traverseCursor(cursor, action);
}

export function traverseCollectionCursor(collection, action) {
return async (dispatch, getState) => {
const state = getState();
if (state.entries.getIn(['pages', `${ collection.get('name') }`, 'isFetching',])) {
return;
}
const backend = currentBackend(state.config);

const { action: realAction, append } = appendActions.has(action)
? appendActions.get(action).toJS()
: { action, append: false };
const cursor = selectCollectionEntriesCursor(state.cursors, collection.get('name'));

// Handle cursors representing pages in the old, integer-based
// pagination API
if (cursor.meta.get("usingOldPaginationAPI", false)) {
return dispatch(loadEntries(collection, cursor.data.get("nextPage")));
}

try {
dispatch(entriesLoading(collection));
const { entries, cursor: newCursor } = await traverseCursor(backend, cursor, realAction);
// Pass null for the old pagination argument - this will
// eventually be removed.
return dispatch(entriesLoaded(collection, entries, null, addAppendActionsToCursor(newCursor), append));
} catch (err) {
console.error(err);
dispatch(notifSend({
message: `Failed to persist entry: ${ err }`,
kind: 'danger',
dismissAfter: 8000,
}));
return Promise.reject(dispatch(entriesFailed(collection, err)));
}
}
}

export function createEmptyDraft(collection) {
return (dispatch) => {
const dataFields = {};
Expand Down
133 changes: 28 additions & 105 deletions src/actions/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,121 +105,44 @@ export function clearSearch() {
// SearchEntries will search for complete entries in all collections.
export function searchEntries(searchTerm, page = 0) {
return (dispatch, getState) => {
dispatch(searchingEntries(searchTerm));

const state = getState();
const backend = currentBackend(state.config);
const allCollections = state.collections.keySeq().toArray();
const collections = allCollections.filter(collection => selectIntegration(state, collection, 'search'));
const integration = selectIntegration(state, collections[0], 'search');
if (!integration) {
localSearch(searchTerm, getState, dispatch);
} else {
const provider = getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration);
dispatch(searchingEntries(searchTerm));
provider.search(collections, searchTerm, page).then(
response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)),
error => dispatch(searchFailure(searchTerm, error))
);
}

const searchPromise = integration
? getIntegrationProvider(state.integrations, backend.getToken, integration).search(collections, searchTerm, page)
: backend.search(state.collections.valueSeq().toArray(), searchTerm);

return searchPromise.then(
response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)),
error => dispatch(searchFailure(searchTerm, error))
);
};
}

// Instead of searching for complete entries, query will search for specific fields
// in specific collections and return raw data (no entries).
export function query(namespace, collection, searchFields, searchTerm) {
export function query(namespace, collectionName, searchFields, searchTerm) {
return (dispatch, getState) => {
dispatch(querying(namespace, collectionName, searchFields, searchTerm));

const state = getState();
const integration = selectIntegration(state, collection, 'search');
dispatch(querying(namespace, collection, searchFields, searchTerm));
if (!integration) {
localQuery(namespace, collection, searchFields, searchTerm, state, dispatch);
} else {
const provider = getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration);
provider.searchBy(searchFields.map(f => `data.${ f }`), collection, searchTerm).then(
response => dispatch(querySuccess(namespace, collection, searchFields, searchTerm, response)),
error => dispatch(queryFailure(namespace, collection, searchFields, searchTerm, error))
);
}
const backend = currentBackend(state.config);
const integration = selectIntegration(state, collectionName, 'search');
const collection = state.collections.find(collection => collection.get('name') === collectionName);

const queryPromise = integration
? getIntegrationProvider(state.integrations, backend.getToken, integration)
.searchBy(searchFields.map(f => `data.${ f }`), collectionName, searchTerm)
: backend.query(collection, searchFields, searchTerm);

return queryPromise.then(
response => dispatch(querySuccess(namespace, collectionName, searchFields, searchTerm, response)),
error => dispatch(queryFailure(namespace, collectionName, searchFields, searchTerm, error))
);
};
}

// Local Query & Search functions

function localSearch(searchTerm, getState, dispatch) {
return (function acc(localResults = { entries: [] }) {
function processCollection(collection, collectionKey) {
const state = getState();
if (state.entries.hasIn(['pages', collectionKey, 'ids'])) {
const searchFields = [
selectInferedField(collection, 'title'),
selectInferedField(collection, 'shortTitle'),
selectInferedField(collection, 'author'),
];
const collectionEntries = selectEntries(state, collectionKey).toJS();
const filteredEntries = fuzzy.filter(searchTerm, collectionEntries, {
extract: entry => searchFields.reduce((acc, field) => {
const f = entry.data[field];
return f ? `${ acc } ${ f }` : acc;
}, ""),
}).filter(entry => entry.score > 5);
localResults[collectionKey] = true;
localResults.entries = localResults.entries.concat(filteredEntries);

const returnedKeys = Object.keys(localResults);
const allCollections = state.collections.keySeq().toArray();
if (allCollections.every(v => returnedKeys.indexOf(v) !== -1)) {
const sortedResults = localResults.entries.sort((a, b) => {
if (a.score > b.score) return -1;
if (a.score < b.score) return 1;
return 0;
}).map(f => f.original);
if (allCollections.size > 3 || localResults.entries.length > 30) {
console.warn('The Netlify CMS is currently using a Built-in search.' +
'\nWhile this works great for small sites, bigger projects might benefit from a separate search integration.' +
'\nPlease refer to the documentation for more information');
}
dispatch(searchSuccess(searchTerm, sortedResults, 0));
}
} else {
// Collection entries aren't loaded yet.
// Dispatch loadEntries and wait before redispatching this action again.
dispatch({
type: WAIT_UNTIL_ACTION,
predicate: action => (action.type === ENTRIES_SUCCESS && action.payload.collection === collectionKey),
run: () => processCollection(collection, collectionKey),
});
dispatch(loadEntries(collection));
}
}
getState().collections.forEach(processCollection);
}());
}


function localQuery(namespace, collection, searchFields, searchTerm, state, dispatch) {
// Check if entries in this collection were already loaded
if (state.entries.hasIn(['pages', collection, 'ids'])) {
const entries = selectEntries(state, collection).toJS();
const filteredEntries = fuzzy.filter(searchTerm, entries, {
extract: entry => searchFields.reduce((acc, field) => {
const f = entry.data[field];
return f ? `${ acc } ${ f }` : acc;
}, ""),
}).filter(entry => entry.score > 5);

const resultObj = {
query: searchTerm,
hits: [],
};

resultObj.hits = filteredEntries.map(f => f.original);
dispatch(querySuccess(namespace, collection, searchFields, searchTerm, resultObj));
} else {
// Collection entries aren't loaded yet.
// Dispatch loadEntries and wait before redispatching this action again.
dispatch({
type: WAIT_UNTIL_ACTION,
predicate: action => (action.type === ENTRIES_SUCCESS && action.payload.collection === collection),
run: dispatch => dispatch(query(namespace, collection, searchFields, searchTerm)),
});
dispatch(loadEntries(state.collections.get(collection)));
}
}
Loading