diff --git a/.eslintrc b/.eslintrc
index 11ead53f..7a27f928 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,14 +1,12 @@
{
"parser": "babel-eslint",
- "extends": [
- "eslint:recommended",
- "plugin:react/recommended"
- ],
+ "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:cypress/recommended"],
"env": {
"es6": true,
"browser": true,
"node": true,
- "jest": true
+ "jest": true,
+ "cypress/globals": true
},
"globals": {
"NETLIFY_CMS_VERSION": false,
@@ -25,12 +23,10 @@
"emotion/import-from-emotion": "error",
"emotion/styled-import": "error"
},
- "plugins": [
- "emotion",
- ],
+ "plugins": ["emotion", "cypress"],
"settings": {
"react": {
- "version": "detect",
- },
- },
+ "version": "detect"
+ }
+ }
}
diff --git a/.gitignore b/.gitignore
index a8c1129f..4415ed2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@ cypress/screenshots
/coverage/
.cache
*.log
+.env
+.temp/
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
index 485fb2a0..3b3bb211 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,5 @@
dist/
bin/
public/
-.cache/
\ No newline at end of file
+.cache/
+packages/netlify-cms-backend-github/src/fragmentTypes.js
\ No newline at end of file
diff --git a/cypress.json b/cypress.json
index 081f6554..d3527dbe 100644
--- a/cypress.json
+++ b/cypress.json
@@ -1,4 +1,5 @@
{
"baseUrl": "http://localhost:8080",
- "projectId": "dzqjxb"
+ "projectId": "dzqjxb",
+ "testFiles": "*spec*.js"
}
diff --git a/cypress/integration/editorial_workflow_spec.js b/cypress/integration/editorial_workflow_spec.js
deleted file mode 100644
index 890cd0f5..00000000
--- a/cypress/integration/editorial_workflow_spec.js
+++ /dev/null
@@ -1,349 +0,0 @@
-import '../utils/dismiss-local-backup';
-
-describe('Editorial Workflow', () => {
- const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
- const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
- const entry1 = { title: 'first title', body: 'first body' };
- const entry2 = { title: 'second title', body: 'second body' };
- const entry3 = { title: 'third title', body: 'third body' };
- const setting1 = { limit: 10, author: 'John Doe' };
- const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
- const notifications = {
- saved: 'Entry saved',
- published: 'Entry published',
- updated: 'Entry status updated',
- deletedUnpublished: 'Unpublished changes deleted',
- error: {
- missingField: "Oops, you've missed a required field. Please complete before saving.",
- },
- validation: {
- range: {
- fieldLabel: 'Number of posts on frontpage',
- message: 'Number of posts on frontpage must be between 1 and 10.',
- },
- },
- };
-
- describe('Test Backend', () => {
- function login() {
- cy.viewport(1200, 1200);
- cy.visit('/');
- cy.contains('button', 'Login').click();
- }
-
- function createPost({ title, body }) {
- cy.contains('a', 'New Post').click();
- cy.get('input')
- .first()
- .type(title);
- cy.get('[data-slate-editor]')
- .click()
- .type(body);
- cy.get('input')
- .first()
- .click();
- cy.contains('button', 'Save').click();
- assertNotification(notifications.saved);
- }
-
- function validateObjectFields({ limit, author }) {
- cy.get('a[href^="#/collections/settings"]').click();
- cy.get('a[href^="#/collections/settings/entries/general"]').click();
- cy.get('input[type=number]').type(limit);
- cy.contains('button', 'Save').click();
- assertNotification(notifications.error.missingField);
- cy.contains('label', 'Default Author').click();
- cy.focused().type(author);
- cy.contains('button', 'Save').click();
- assertNotification(notifications.saved);
- }
-
- function validateNestedObjectFields({ limit, author }) {
- cy.get('a[href^="#/collections/settings"]').click();
- cy.get('a[href^="#/collections/settings/entries/general"]').click();
- cy.contains('label', 'Default Author').click();
- cy.focused().type(author);
- cy.contains('button', 'Save').click();
- assertNotification(notifications.error.missingField);
- cy.get('input[type=number]').type(limit + 1);
- cy.contains('button', 'Save').click();
- assertFieldValidationError(notifications.validation.range);
- cy.get('input[type=number]')
- .clear()
- .type(-1);
- cy.contains('button', 'Save').click();
- assertFieldValidationError(notifications.validation.range);
- cy.get('input[type=number]')
- .clear()
- .type(limit);
- cy.contains('button', 'Save').click();
- assertNotification(notifications.saved);
- }
-
- function validateListFields({ name, description }) {
- cy.get('a[href^="#/collections/settings"]').click();
- cy.get('a[href^="#/collections/settings/entries/authors"]').click();
- cy.contains('button', 'Add').click();
- cy.contains('button', 'Save').click();
- assertNotification(notifications.error.missingField);
- cy.get('input')
- .eq(2)
- .type(name);
- cy.get('[data-slate-editor]')
- .eq(2)
- .type(description);
- cy.contains('button', 'Save').click();
- }
-
- function exitEditor() {
- cy.contains('a[href^="#/collections/"]', 'Writing in').click();
- }
-
- function deleteEntryInEditor() {
- cy.contains('button', 'Delete').click();
- assertNotification(notifications.deletedUnpublished);
- }
-
- function assertEntryDeleted(entry) {
- if (Array.isArray(entry)) {
- const titles = entry.map(e => e.title);
- cy.get('a h2').each((el, idx) => {
- expect(titles).not.to.include(el.text());
- });
- } else {
- cy.get('a h2').each((el, idx) => {
- expect(entry.title).not.to.equal(el.text());
- });
- }
- }
-
- function createAndDeletePost(entry) {
- createPost(entry);
- deleteEntryInEditor();
- }
-
- function createPostAndExit(entry) {
- createPost(entry);
- exitEditor();
- }
-
- function createPublishedPost(entry) {
- createPost(entry);
- updateWorkflowStatusInEditor(editorStatus.ready);
- publishInEditor();
- }
-
- function createPublishedPostAndExit(entry) {
- createPublishedPost(entry);
- exitEditor();
- }
-
- function validateObjectFieldsAndExit(setting) {
- validateObjectFields(setting);
- exitEditor();
- }
-
- function validateNestedObjectFieldsAndExit(setting) {
- validateNestedObjectFields(setting);
- exitEditor();
- }
-
- function validateListFieldsAndExit(setting) {
- validateListFields(setting);
- exitEditor();
- }
-
- function goToWorkflow() {
- cy.contains('a', 'Workflow').click();
- }
-
- function goToCollections() {
- cy.contains('a', 'Content').click();
- }
-
- function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
- cy.contains('h2', fromColumnHeading)
- .parent()
- .contains('a', title)
- .trigger('dragstart', {
- dataTransfer: {},
- force: true,
- });
- cy.contains('h2', toColumnHeading)
- .parent()
- .trigger('drop', {
- dataTransfer: {},
- force: true,
- });
- assertNotification(notifications.updated);
- }
-
- function assertWorkflowStatus({ title }, status) {
- cy.contains('h2', status)
- .parent()
- .contains('a', title);
- }
-
- function updateWorkflowStatusInEditor(newStatus) {
- cy.contains('[role="button"]', 'Set status').as('setStatusButton');
- cy.get('@setStatusButton')
- .parent()
- .within(() => {
- cy.get('@setStatusButton').click();
- cy.contains('[role="menuitem"] span', newStatus).click();
- });
- assertNotification(notifications.updated);
- }
-
- function assertWorkflowStatusInEditor(status) {
- cy.contains('[role="button"]', 'Set status').as('setStatusButton');
- cy.get('@setStatusButton')
- .parent()
- .within(() => {
- cy.get('@setStatusButton').click();
- cy.contains('[role="menuitem"] span', status)
- .parent()
- .within(() => {
- cy.get('svg');
- });
- cy.get('@setStatusButton').click();
- });
- }
-
- function publishWorkflowEntry({ title }) {
- cy.contains('h2', workflowStatus.ready)
- .parent()
- .within(() => {
- cy.contains('a', title)
- .parent()
- .within(() => {
- cy.contains('button', 'Publish new entry').click({ force: true });
- });
- });
- assertNotification(notifications.published);
- }
-
- function assertPublishedEntry(entry) {
- if (Array.isArray(entry)) {
- const entries = entry.reverse();
- cy.get('a h2').then(els => {
- cy.wrap(els.slice(0, entries.length)).each((el, idx) => {
- cy.wrap(el).contains(entries[idx].title);
- });
- });
- } else {
- cy.get('a h2')
- .first()
- .contains(entry.title);
- }
- }
-
- function assertNotification(message) {
- if (Array.isArray(message)) {
- const messages = message.reverse();
- cy.get('.notif__container div')
- .should('have.length.of', messages.length)
- .each((el, idx) => {
- cy.wrap(el)
- .contains(messages[idx])
- .invoke('hide');
- });
- } else {
- cy.get('.notif__container').within(() => {
- cy.contains(message).invoke('hide');
- });
- }
- }
-
- function assertFieldValidationError({ message, fieldLabel }) {
- cy.contains('label', fieldLabel)
- .siblings('ul[class*=ControlErrorsList]')
- .contains(message);
- }
-
- function assertOnCollectionsPage() {
- cy.url().should('contain', '/#/collections/posts');
- cy.contains('h2', 'Collections');
- }
-
- it('successfully loads', () => {
- login();
- });
-
- it('can create an entry', () => {
- login();
- createPostAndExit(entry1);
- });
-
- it('can validate object fields', () => {
- login();
- validateObjectFieldsAndExit(setting1);
- });
-
- it('can validate fields nested in an object field', () => {
- login();
- validateNestedObjectFieldsAndExit(setting1);
- });
-
- it('can validate list fields', () => {
- login();
- validateListFieldsAndExit(setting2);
- });
-
- it('can publish an editorial workflow entry', () => {
- login();
- createPostAndExit(entry1);
- goToWorkflow();
- updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
- publishWorkflowEntry(entry1);
- });
-
- it('can change workflow status', () => {
- login();
- createPostAndExit(entry1);
- goToWorkflow();
- updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
- updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
- updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
- updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
- updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
- });
-
- it('can change status on and publish multiple entries', () => {
- login();
- createPostAndExit(entry1);
- createPostAndExit(entry2);
- createPostAndExit(entry3);
- goToWorkflow();
- updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
- updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
- updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
- publishWorkflowEntry(entry3);
- publishWorkflowEntry(entry2);
- publishWorkflowEntry(entry1);
- goToCollections();
- assertPublishedEntry([entry3, entry2, entry1]);
- });
-
- it('can delete an entry', () => {
- login();
- createPost(entry1);
- deleteEntryInEditor();
- assertOnCollectionsPage();
- assertEntryDeleted(entry1);
- });
-
- it('can update workflow status from within the editor', () => {
- login();
- createPost(entry1);
- assertWorkflowStatusInEditor(editorStatus.draft);
- updateWorkflowStatusInEditor(editorStatus.review);
- assertWorkflowStatusInEditor(editorStatus.review);
- updateWorkflowStatusInEditor(editorStatus.ready);
- assertWorkflowStatusInEditor(editorStatus.ready);
- exitEditor();
- goToWorkflow();
- assertWorkflowStatus(entry1, workflowStatus.ready);
- });
- });
-});
diff --git a/cypress/integration/editorial_workflow_spec_github_backend_graphql.js b/cypress/integration/editorial_workflow_spec_github_backend_graphql.js
new file mode 100644
index 00000000..a0be721b
--- /dev/null
+++ b/cypress/integration/editorial_workflow_spec_github_backend_graphql.js
@@ -0,0 +1,5 @@
+import fixture from './github/editorial_workflow';
+
+describe.skip('Github Backend Editorial Workflow - GraphQL API', () => {
+ fixture({ use_graphql: true });
+});
diff --git a/cypress/integration/editorial_workflow_spec_github_backend_graphql_open_authoring.js b/cypress/integration/editorial_workflow_spec_github_backend_graphql_open_authoring.js
new file mode 100644
index 00000000..6f953725
--- /dev/null
+++ b/cypress/integration/editorial_workflow_spec_github_backend_graphql_open_authoring.js
@@ -0,0 +1,5 @@
+import fixture from './github/open_authoring';
+
+describe.skip('Github Backend Editorial Workflow - GraphQL API - Open Authoring', () => {
+ fixture({ use_graphql: true });
+});
diff --git a/cypress/integration/editorial_workflow_spec_github_backend_rest.js b/cypress/integration/editorial_workflow_spec_github_backend_rest.js
new file mode 100644
index 00000000..cb255003
--- /dev/null
+++ b/cypress/integration/editorial_workflow_spec_github_backend_rest.js
@@ -0,0 +1,5 @@
+import fixture from './github/editorial_workflow';
+
+describe.skip('Github Backend Editorial Workflow - REST API', () => {
+ fixture({ use_graphql: false });
+});
diff --git a/cypress/integration/editorial_workflow_spec_github_backend_rest_open_authoring.js b/cypress/integration/editorial_workflow_spec_github_backend_rest_open_authoring.js
new file mode 100644
index 00000000..4a547cad
--- /dev/null
+++ b/cypress/integration/editorial_workflow_spec_github_backend_rest_open_authoring.js
@@ -0,0 +1,5 @@
+import fixture from './github/open_authoring';
+
+describe.skip('Github Backend Editorial Workflow - REST API - Open Authoring', () => {
+ fixture({ use_graphql: false });
+});
diff --git a/cypress/integration/editorial_workflow_spec_test_backend.js b/cypress/integration/editorial_workflow_spec_test_backend.js
new file mode 100644
index 00000000..192b0e3e
--- /dev/null
+++ b/cypress/integration/editorial_workflow_spec_test_backend.js
@@ -0,0 +1,126 @@
+import '../utils/dismiss-local-backup';
+import {
+ login,
+ createPost,
+ createPostAndExit,
+ exitEditor,
+ goToWorkflow,
+ goToCollections,
+ updateWorkflowStatus,
+ publishWorkflowEntry,
+ assertWorkflowStatusInEditor,
+ assertPublishedEntry,
+ deleteEntryInEditor,
+ assertOnCollectionsPage,
+ assertEntryDeleted,
+ assertWorkflowStatus,
+ updateWorkflowStatusInEditor,
+ validateObjectFieldsAndExit,
+ validateNestedObjectFieldsAndExit,
+ validateListFieldsAndExit,
+} from '../utils/steps';
+import { setting1, setting2, workflowStatus, editorStatus } from '../utils/constants';
+
+const entry1 = {
+ title: 'first title',
+ body: 'first body',
+};
+const entry2 = {
+ title: 'second title',
+ body: 'second body',
+};
+const entry3 = {
+ title: 'third title',
+ body: 'third body',
+};
+
+describe('Test Backend Editorial Workflow', () => {
+ after(() => {
+ cy.task('restoreDefaults');
+ });
+
+ before(() => {
+ Cypress.config('defaultCommandTimeout', 4000);
+ cy.task('setupBackend', { backend: 'test' });
+ });
+
+ it('successfully loads', () => {
+ login();
+ });
+
+ it('can create an entry', () => {
+ login();
+ createPostAndExit(entry1);
+ });
+
+ it('can validate object fields', () => {
+ login();
+ validateObjectFieldsAndExit(setting1);
+ });
+
+ it('can validate fields nested in an object field', () => {
+ login();
+ validateNestedObjectFieldsAndExit(setting1);
+ });
+
+ it('can validate list fields', () => {
+ login();
+ validateListFieldsAndExit(setting2);
+ });
+
+ it('can publish an editorial workflow entry', () => {
+ login();
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ publishWorkflowEntry(entry1);
+ });
+
+ it('can change workflow status', () => {
+ login();
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
+ updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
+ updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
+ updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ });
+
+ it('can change status on and publish multiple entries', () => {
+ login();
+ createPostAndExit(entry1);
+ createPostAndExit(entry2);
+ createPostAndExit(entry3);
+ goToWorkflow();
+ updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
+ updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ publishWorkflowEntry(entry3);
+ publishWorkflowEntry(entry2);
+ publishWorkflowEntry(entry1);
+ goToCollections();
+ assertPublishedEntry([entry3, entry2, entry1]);
+ });
+
+ it('can delete an entry', () => {
+ login();
+ createPost(entry1);
+ deleteEntryInEditor();
+ assertOnCollectionsPage();
+ assertEntryDeleted(entry1);
+ });
+
+ it('can update workflow status from within the editor', () => {
+ login();
+ createPost(entry1);
+ assertWorkflowStatusInEditor(editorStatus.draft);
+ updateWorkflowStatusInEditor(editorStatus.review);
+ assertWorkflowStatusInEditor(editorStatus.review);
+ updateWorkflowStatusInEditor(editorStatus.ready);
+ assertWorkflowStatusInEditor(editorStatus.ready);
+ exitEditor();
+ goToWorkflow();
+ assertWorkflowStatus(entry1, workflowStatus.ready);
+ });
+});
diff --git a/cypress/integration/github/editorial_workflow.js b/cypress/integration/github/editorial_workflow.js
new file mode 100644
index 00000000..ae09688a
--- /dev/null
+++ b/cypress/integration/github/editorial_workflow.js
@@ -0,0 +1,117 @@
+import '../../utils/dismiss-local-backup';
+import {
+ login,
+ createPost,
+ createPostAndExit,
+ updateExistingPostAndExit,
+ exitEditor,
+ goToWorkflow,
+ goToCollections,
+ updateWorkflowStatus,
+ publishWorkflowEntry,
+ assertWorkflowStatusInEditor,
+ assertPublishedEntry,
+ deleteEntryInEditor,
+ assertOnCollectionsPage,
+ assertEntryDeleted,
+ assertWorkflowStatus,
+ updateWorkflowStatusInEditor,
+} from '../../utils/steps';
+import { workflowStatus, editorStatus } from '../../utils/constants';
+import { entry1, entry2, entry3 } from './entries';
+
+export default function({ use_graphql }) {
+ let taskResult = { data: {} };
+
+ const backend = 'github';
+
+ before(() => {
+ Cypress.config('taskTimeout', 1200000);
+ Cypress.config('defaultCommandTimeout', 60000);
+ cy.task('setupBackend', { backend }).then(data => {
+ taskResult.data = data;
+ });
+
+ cy.task('updateBackendOptions', { backend, options: { use_graphql, open_authoring: false } });
+ });
+
+ after(() => {
+ cy.task('teardownBackend', { backend, ...taskResult.data });
+ cy.task('restoreDefaults');
+ });
+
+ afterEach(() => {
+ cy.task('teardownBackendTest', { backend, ...taskResult.data });
+ });
+
+ it('successfully loads', () => {
+ login(taskResult.data.user);
+ });
+
+ it('can create an entry', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ });
+
+ it('can update an entry', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ updateExistingPostAndExit(entry1, entry2);
+ });
+
+ it('can publish an editorial workflow entry', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ publishWorkflowEntry(entry1);
+ });
+
+ it('can change workflow status', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
+ updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.ready);
+ updateWorkflowStatus(entry1, workflowStatus.ready, workflowStatus.review);
+ updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ });
+
+ it('can change status on and publish multiple entries', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ createPostAndExit(entry2);
+ createPostAndExit(entry3);
+ goToWorkflow();
+ updateWorkflowStatus(entry3, workflowStatus.draft, workflowStatus.ready);
+ updateWorkflowStatus(entry2, workflowStatus.draft, workflowStatus.ready);
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ publishWorkflowEntry(entry3);
+ publishWorkflowEntry(entry2);
+ publishWorkflowEntry(entry1);
+ goToCollections();
+ assertPublishedEntry([entry3, entry2, entry1]);
+ });
+
+ it('can delete an entry', () => {
+ login(taskResult.data.user);
+ createPost(entry1);
+ deleteEntryInEditor();
+ assertOnCollectionsPage();
+ assertEntryDeleted(entry1);
+ });
+
+ it('can update workflow status from within the editor', () => {
+ login(taskResult.data.user);
+ createPost(entry1);
+ assertWorkflowStatusInEditor(editorStatus.draft);
+ updateWorkflowStatusInEditor(editorStatus.review);
+ assertWorkflowStatusInEditor(editorStatus.review);
+ updateWorkflowStatusInEditor(editorStatus.ready);
+ assertWorkflowStatusInEditor(editorStatus.ready);
+ exitEditor();
+ goToWorkflow();
+ assertWorkflowStatus(entry1, workflowStatus.ready);
+ });
+}
diff --git a/cypress/integration/github/entries.js b/cypress/integration/github/entries.js
new file mode 100644
index 00000000..d27a2a41
--- /dev/null
+++ b/cypress/integration/github/entries.js
@@ -0,0 +1,21 @@
+export const entry1 = {
+ title: 'first title',
+ body: 'first body',
+ description: 'first description',
+ category: 'first category',
+ tags: 'tag1',
+};
+export const entry2 = {
+ title: 'second title',
+ body: 'second body',
+ description: 'second description',
+ category: 'second category',
+ tags: 'tag2',
+};
+export const entry3 = {
+ title: 'third title',
+ body: 'third body',
+ description: 'third description',
+ category: 'third category',
+ tags: 'tag3',
+};
diff --git a/cypress/integration/github/open_authoring.js b/cypress/integration/github/open_authoring.js
new file mode 100644
index 00000000..6c94dae1
--- /dev/null
+++ b/cypress/integration/github/open_authoring.js
@@ -0,0 +1,99 @@
+import '../../utils/dismiss-local-backup';
+import {
+ login,
+ createPostAndExit,
+ updateExistingPostAndExit,
+ goToWorkflow,
+ deleteWorkflowEntry,
+ updateWorkflowStatus,
+ publishWorkflowEntry,
+} from '../../utils/steps';
+import { workflowStatus } from '../../utils/constants';
+import { entry1, entry2 } from './entries';
+
+export default function({ use_graphql }) {
+ let taskResult = { data: {} };
+
+ const backend = 'github';
+
+ before(() => {
+ Cypress.config('taskTimeout', 1200000);
+ Cypress.config('defaultCommandTimeout', 60000);
+ cy.task('setupBackend', { backend }).then(data => {
+ taskResult.data = data;
+ });
+ cy.task('updateBackendOptions', { backend, options: { use_graphql, open_authoring: true } });
+ });
+
+ after(() => {
+ cy.task('teardownBackend', { backend, ...taskResult.data });
+ cy.task('restoreDefaults');
+ });
+
+ afterEach(() => {
+ cy.task('teardownBackendTest', { backend, ...taskResult.data });
+ });
+
+ it('successfully loads', () => {
+ login(taskResult.data.user);
+ });
+
+ it('can create an entry', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ });
+
+ it('can update an entry', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ updateExistingPostAndExit(entry1, entry2);
+ });
+
+ it('can publish an editorial workflow entry', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
+ publishWorkflowEntry(entry1);
+ });
+
+ it('successfully forks repository and loads', () => {
+ login(taskResult.data.forkUser);
+ });
+
+ it('can create an entry on fork', () => {
+ login(taskResult.data.forkUser);
+ createPostAndExit(entry1);
+ });
+
+ it('can update a draft entry on fork', () => {
+ login(taskResult.data.user);
+ createPostAndExit(entry1);
+ updateExistingPostAndExit(entry1, entry2);
+ });
+
+ it('can change entry status from fork', () => {
+ login(taskResult.data.forkUser);
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
+ });
+
+ it('can delete review entry from fork', () => {
+ login(taskResult.data.forkUser);
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
+ deleteWorkflowEntry(entry1);
+ });
+
+ it('can return entry to draft and delete it', () => {
+ login(taskResult.data.forkUser);
+ createPostAndExit(entry1);
+ goToWorkflow();
+ updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.review);
+
+ updateWorkflowStatus(entry1, workflowStatus.review, workflowStatus.draft);
+ deleteWorkflowEntry(entry1);
+ });
+}
diff --git a/cypress/plugins/github.js b/cypress/plugins/github.js
new file mode 100644
index 00000000..2c6518da
--- /dev/null
+++ b/cypress/plugins/github.js
@@ -0,0 +1,193 @@
+const Octokit = require('@octokit/rest');
+const fs = require('fs-extra');
+const path = require('path');
+const simpleGit = require('simple-git/promise');
+
+const GIT_SSH_COMMAND = 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no';
+
+function getGitHubClient(token) {
+ const client = new Octokit({
+ auth: `token ${token}`,
+ baseUrl: 'https://api.github.com',
+ });
+ return client;
+}
+
+function getEnvs() {
+ const {
+ GITHUB_REPO_OWNER: owner,
+ GITHUB_REPO_NAME: repo,
+ GITHUB_REPO_TOKEN: token,
+ GITHUB_OPEN_AUTHORING_OWNER: forkOwner,
+ GITHUB_OPEN_AUTHORING_TOKEN: forkToken,
+ } = process.env;
+ if (!owner || !repo || !token || !forkOwner || !forkToken) {
+ throw new Error(
+ 'Please set GITHUB_REPO_OWNER, GITHUB_REPO_NAME, GITHUB_REPO_TOKEN, GITHUB_OPEN_AUTHORING_OWNER, GITHUB_OPEN_AUTHORING_TOKEN environment variables',
+ );
+ }
+ return { owner, repo, token, forkOwner, forkToken };
+}
+
+async function prepareTestGitHubRepo() {
+ const { owner, repo, token } = getEnvs();
+
+ // postfix a random string to avoid collisions
+ const postfix = Math.random()
+ .toString(32)
+ .substring(2);
+ const testRepoName = `${repo}-${Date.now()}-${postfix}`;
+
+ const client = getGitHubClient(token);
+
+ console.log('Creating repository', testRepoName);
+ await client.repos.createForAuthenticatedUser({
+ name: testRepoName,
+ });
+
+ const tempDir = path.join('.temp', testRepoName);
+ await fs.remove(tempDir);
+ let git = simpleGit().env({ ...process.env, GIT_SSH_COMMAND });
+
+ const repoUrl = `git@github.com:${owner}/${repo}.git`;
+
+ console.log('Cloning repository', repoUrl);
+ await git.clone(repoUrl, tempDir);
+ git = simpleGit(tempDir).env({ ...process.env, GIT_SSH_COMMAND });
+
+ console.log('Pushing to new repository', testRepoName);
+
+ await git.removeRemote('origin');
+ await git.addRemote(
+ 'origin',
+ `https://${token}:x-oauth-basic@github.com/${owner}/${testRepoName}`,
+ );
+ await git.push(['-u', 'origin', 'master']);
+
+ return { owner, repo: testRepoName, tempDir };
+}
+
+async function getAuthenticatedUser(token) {
+ const client = getGitHubClient(token);
+ const { data: user } = await client.users.getAuthenticated();
+ return { ...user, token, backendName: 'github' };
+}
+
+async function getUser() {
+ const { token } = getEnvs();
+ return getAuthenticatedUser(token);
+}
+
+async function getForkUser() {
+ const { forkToken } = getEnvs();
+ return getAuthenticatedUser(forkToken);
+}
+
+async function deleteRepositories({ owner, repo, tempDir }) {
+ const { forkOwner, token, forkToken } = getEnvs();
+
+ const errorHandler = e => {
+ if (e.status !== 404) {
+ throw e;
+ }
+ };
+
+ console.log('Deleting repository', `${owner}/${repo}`);
+ await fs.remove(tempDir);
+
+ let client = getGitHubClient(token);
+ await client.repos
+ .delete({
+ owner,
+ repo,
+ })
+ .catch(errorHandler);
+
+ console.log('Deleting forked repository', `${forkOwner}/${repo}`);
+ client = getGitHubClient(forkToken);
+ await client.repos
+ .delete({
+ owner: forkOwner,
+ repo,
+ })
+ .catch(errorHandler);
+}
+
+async function resetOriginRepo({ owner, repo, tempDir }) {
+ console.log('Resetting origin repo:', `${owner}/repo`);
+ const { token } = getEnvs();
+ const client = getGitHubClient(token);
+
+ const { data: prs } = await client.pulls.list({
+ repo,
+ owner,
+ state: 'open',
+ });
+ const numbers = prs.map(pr => pr.number);
+ console.log('Closing prs:', numbers);
+ await Promise.all(
+ numbers.map(pull_number =>
+ client.pulls.update({
+ owner,
+ repo,
+ pull_number,
+ }),
+ ),
+ );
+
+ const { data: branches } = await client.repos.listBranches({ owner, repo });
+ const refs = branches.filter(b => b.name !== 'master').map(b => `heads/${b.name}`);
+
+ console.log('Deleting refs', refs);
+ await Promise.all(
+ refs.map(ref =>
+ client.git.deleteRef({
+ owner,
+ repo,
+ ref,
+ }),
+ ),
+ );
+
+ console.log('Resetting master');
+ const git = simpleGit(tempDir).env({ ...process.env, GIT_SSH_COMMAND });
+ await git.push(['--force', 'origin', 'master']);
+ console.log('Done resetting origin repo:', `${owner}/repo`);
+}
+
+async function resetForkedRepo({ repo }) {
+ const { forkToken, forkOwner } = getEnvs();
+ const client = getGitHubClient(forkToken);
+
+ const { data: repos } = await client.repos.list();
+ if (repos.some(r => r.name === repo)) {
+ console.log('Resetting forked repo:', `${forkOwner}/${repo}`);
+ const { data: branches } = await client.repos.listBranches({ owner: forkOwner, repo });
+ const refs = branches.filter(b => b.name !== 'master').map(b => `heads/${b.name}`);
+
+ console.log('Deleting refs', refs);
+ await Promise.all(
+ refs.map(ref =>
+ client.git.deleteRef({
+ owner: forkOwner,
+ repo,
+ ref,
+ }),
+ ),
+ );
+ console.log('Done resetting forked repo:', `${forkOwner}/repo`);
+ }
+}
+
+async function resetRepositories({ owner, repo, tempDir }) {
+ await resetOriginRepo({ owner, repo, tempDir });
+ await resetForkedRepo({ repo });
+}
+
+module.exports = {
+ prepareTestGitHubRepo,
+ deleteRepositories,
+ getUser,
+ getForkUser,
+ resetRepositories,
+};
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index fd170fba..e515b70e 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -10,8 +10,106 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
+require('dotenv').config();
+const fs = require('fs-extra');
+const path = require('path');
+const yaml = require('js-yaml');
+const {
+ prepareTestGitHubRepo,
+ deleteRepositories,
+ getUser,
+ getForkUser,
+ resetRepositories,
+} = require('./github');
-module.exports = (on, config) => {
- // `on` is used to hook into various events Cypress emits
- // `config` is the resolved Cypress config
+const devTestDirectory = path.join(__dirname, '..', '..', 'dev-test');
+const backendsDirectory = path.join(devTestDirectory, 'backends');
+
+async function copyBackendFiles(backend) {
+ for (let file of ['config.yml', 'index.html']) {
+ await fs.copyFile(
+ path.join(backendsDirectory, backend, file),
+ path.join(devTestDirectory, file),
+ );
+ }
}
+
+async function updateConfig(configModifier) {
+ const configFile = path.join(devTestDirectory, 'config.yml');
+ const configContent = await fs.readFile(configFile);
+ const config = yaml.safeLoad(configContent);
+ await configModifier(config);
+ await fs.writeFileSync(configFile, yaml.safeDump(config));
+ return null;
+}
+
+async function setupGitHub() {
+ const [user, forkUser, repoData] = await Promise.all([
+ getUser(),
+ getForkUser(),
+ prepareTestGitHubRepo(),
+ ]);
+
+ await updateConfig(config => {
+ config.backend.repo = `${repoData.owner}/${repoData.repo}`;
+ });
+
+ return { ...repoData, user, forkUser };
+}
+
+async function teardownGitHub(taskData) {
+ await deleteRepositories(taskData);
+ return null;
+}
+
+async function teardownBackendTest(taskData) {
+ await resetRepositories(taskData);
+ return null;
+}
+
+module.exports = async on => {
+ // `on` is used to hook into various events Cypress emits
+ on('task', {
+ async setupBackend({ backend }) {
+ console.log('Preparing environment for backend', backend);
+ await copyBackendFiles(backend);
+
+ if (backend === 'github') {
+ return await setupGitHub();
+ }
+
+ return null;
+ },
+ async teardownBackend(taskData) {
+ const { backend } = taskData;
+ console.log('Tearing down backend', backend);
+ if (backend === 'github') {
+ return await teardownGitHub(taskData);
+ }
+
+ return null;
+ },
+ async teardownBackendTest(taskData) {
+ const { backend } = taskData;
+ console.log('Tearing down single test for backend', backend);
+ if (backend === 'github') {
+ return await teardownBackendTest(taskData);
+ }
+ },
+ async updateBackendOptions({ backend, options }) {
+ console.log('Updating backend', backend, 'with options', options);
+ if (backend === 'github') {
+ return await updateConfig(config => {
+ config.backend = { ...config.backend, ...options };
+ });
+ }
+ return null;
+ },
+ async restoreDefaults() {
+ console.log('Restoring defaults');
+ await copyBackendFiles('test');
+
+ return null;
+ },
+ });
+};
diff --git a/cypress/utils/constants.js b/cypress/utils/constants.js
new file mode 100644
index 00000000..d152168a
--- /dev/null
+++ b/cypress/utils/constants.js
@@ -0,0 +1,27 @@
+const workflowStatus = { draft: 'Drafts', review: 'In Review', ready: 'Ready' };
+const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
+const setting1 = { limit: 10, author: 'John Doe' };
+const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
+const notifications = {
+ saved: 'Entry saved',
+ published: 'Entry published',
+ updated: 'Entry status updated',
+ deletedUnpublished: 'Unpublished changes deleted',
+ error: {
+ missingField: "Oops, you've missed a required field. Please complete before saving.",
+ },
+ validation: {
+ range: {
+ fieldLabel: 'Number of posts on frontpage',
+ message: 'Number of posts on frontpage must be between 1 and 10.',
+ },
+ },
+};
+
+module.exports = {
+ workflowStatus,
+ editorStatus,
+ setting1,
+ setting2,
+ notifications,
+};
diff --git a/cypress/utils/steps.js b/cypress/utils/steps.js
new file mode 100644
index 00000000..7899b65a
--- /dev/null
+++ b/cypress/utils/steps.js
@@ -0,0 +1,292 @@
+const { notifications, workflowStatus } = require('./constants');
+
+function login(user) {
+ cy.viewport(1200, 1200);
+ if (user) {
+ cy.visit('/', {
+ onBeforeLoad: () => {
+ window.localStorage.setItem('netlify-cms-user', JSON.stringify(user));
+ },
+ });
+ } else {
+ cy.visit('/');
+ cy.contains('button', 'Login').click();
+ }
+}
+
+function assertNotification(message) {
+ if (Array.isArray(message)) {
+ console.log(message);
+ const messages = message.reverse();
+ cy.get('.notif__container div')
+ .should('have.length.of', messages.length)
+ .each((el, idx) => {
+ cy.wrap(el)
+ .contains(messages[idx])
+ .invoke('hide');
+ });
+ } else {
+ cy.get('.notif__container').within(() => {
+ cy.contains(message).invoke('hide');
+ });
+ }
+}
+
+function exitEditor() {
+ cy.contains('a[href^="#/collections/"]', 'Writing in').click();
+}
+
+function goToWorkflow() {
+ cy.contains('a', 'Workflow').click();
+}
+
+function goToCollections() {
+ cy.contains('a', 'Content').click();
+}
+
+function updateWorkflowStatus({ title }, fromColumnHeading, toColumnHeading) {
+ cy.contains('h2', fromColumnHeading)
+ .parent()
+ .contains('a', title)
+ .trigger('dragstart', {
+ dataTransfer: {},
+ force: true,
+ });
+ cy.contains('h2', toColumnHeading)
+ .parent()
+ .trigger('drop', {
+ dataTransfer: {},
+ force: true,
+ });
+ assertNotification(notifications.updated);
+}
+
+function publishWorkflowEntry({ title }) {
+ cy.contains('h2', workflowStatus.ready)
+ .parent()
+ .within(() => {
+ cy.contains('a', title)
+ .parent()
+ .within(() => {
+ cy.contains('button', 'Publish new entry').click({ force: true });
+ });
+ });
+ assertNotification(notifications.published);
+}
+
+function deleteWorkflowEntry({ title }) {
+ cy.contains('a', title)
+ .parent()
+ .within(() => {
+ cy.contains('button', 'Delete new entry').click({ force: true });
+ });
+
+ assertNotification(notifications.deletedUnpublished);
+}
+
+function assertWorkflowStatusInEditor(status) {
+ cy.contains('[role="button"]', 'Set status').as('setStatusButton');
+ cy.get('@setStatusButton')
+ .parent()
+ .within(() => {
+ cy.get('@setStatusButton').click();
+ cy.contains('[role="menuitem"] span', status)
+ .parent()
+ .within(() => {
+ cy.get('svg');
+ });
+ cy.get('@setStatusButton').click();
+ });
+}
+
+function assertPublishedEntry(entry) {
+ if (Array.isArray(entry)) {
+ const entries = entry.reverse();
+ cy.get('a h2').then(els => {
+ cy.wrap(els.slice(0, entries.length)).each((el, idx) => {
+ cy.wrap(el).contains(entries[idx].title);
+ });
+ });
+ } else {
+ cy.get('a h2')
+ .first()
+ .contains(entry.title);
+ }
+}
+
+function deleteEntryInEditor() {
+ cy.contains('button', 'Delete').click();
+ assertNotification(notifications.deletedUnpublished);
+}
+
+function assertOnCollectionsPage() {
+ cy.url().should('contain', '/#/collections/posts');
+ cy.contains('h2', 'Collections');
+}
+
+function assertEntryDeleted(entry) {
+ if (Array.isArray(entry)) {
+ const titles = entry.map(e => e.title);
+ cy.get('a h2').each(el => {
+ expect(titles).not.to.include(el.text());
+ });
+ } else {
+ cy.get('a h2').each(el => {
+ expect(entry.title).not.to.equal(el.text());
+ });
+ }
+}
+
+function assertWorkflowStatus({ title }, status) {
+ cy.contains('h2', status)
+ .parent()
+ .contains('a', title);
+}
+
+function updateWorkflowStatusInEditor(newStatus) {
+ cy.contains('[role="button"]', 'Set status').as('setStatusButton');
+ cy.get('@setStatusButton')
+ .parent()
+ .within(() => {
+ cy.get('@setStatusButton').click();
+ cy.contains('[role="menuitem"] span', newStatus).click();
+ });
+ assertNotification(notifications.updated);
+}
+
+function populateEntry(entry) {
+ const keys = Object.keys(entry);
+ for (let key of keys) {
+ const value = entry[key];
+ if (key === 'body') {
+ cy.get('[data-slate-editor]')
+ .click()
+ .clear()
+ .type(value);
+ } else {
+ cy.get(`[id^="${key}-field"]`)
+ .clear()
+ .type(value);
+ }
+ }
+
+ cy.get('input')
+ .first()
+ .click();
+ cy.contains('button', 'Save').click();
+ assertNotification(notifications.saved);
+}
+
+function createPost(entry) {
+ cy.contains('a', 'New Post').click();
+ populateEntry(entry);
+}
+
+function createPostAndExit(entry) {
+ createPost(entry);
+ exitEditor();
+}
+
+function updateExistingPostAndExit(fromEntry, toEntry) {
+ goToWorkflow();
+ cy.contains('h2', fromEntry.title)
+ .parent()
+ .click({ force: true });
+ populateEntry(toEntry);
+ exitEditor();
+ goToWorkflow();
+ cy.contains('h2', toEntry.title);
+}
+
+function validateObjectFields({ limit, author }) {
+ cy.get('a[href^="#/collections/settings"]').click();
+ cy.get('a[href^="#/collections/settings/entries/general"]').click();
+ cy.get('input[type=number]').type(limit);
+ cy.contains('button', 'Save').click();
+ assertNotification(notifications.error.missingField);
+ cy.contains('label', 'Default Author').click();
+ cy.focused().type(author);
+ cy.contains('button', 'Save').click();
+ assertNotification(notifications.saved);
+}
+
+function validateNestedObjectFields({ limit, author }) {
+ cy.get('a[href^="#/collections/settings"]').click();
+ cy.get('a[href^="#/collections/settings/entries/general"]').click();
+ cy.contains('label', 'Default Author').click();
+ cy.focused().type(author);
+ cy.contains('button', 'Save').click();
+ assertNotification(notifications.error.missingField);
+ cy.get('input[type=number]').type(limit + 1);
+ cy.contains('button', 'Save').click();
+ assertFieldValidationError(notifications.validation.range);
+ cy.get('input[type=number]')
+ .clear()
+ .type(-1);
+ cy.contains('button', 'Save').click();
+ assertFieldValidationError(notifications.validation.range);
+ cy.get('input[type=number]')
+ .clear()
+ .type(limit);
+ cy.contains('button', 'Save').click();
+ assertNotification(notifications.saved);
+}
+
+function validateListFields({ name, description }) {
+ cy.get('a[href^="#/collections/settings"]').click();
+ cy.get('a[href^="#/collections/settings/entries/authors"]').click();
+ cy.contains('button', 'Add').click();
+ cy.contains('button', 'Save').click();
+ assertNotification(notifications.error.missingField);
+ cy.get('input')
+ .eq(2)
+ .type(name);
+ cy.get('[data-slate-editor]')
+ .eq(2)
+ .type(description);
+ cy.contains('button', 'Save').click();
+}
+
+function validateObjectFieldsAndExit(setting) {
+ validateObjectFields(setting);
+ exitEditor();
+}
+
+function validateNestedObjectFieldsAndExit(setting) {
+ validateNestedObjectFields(setting);
+ exitEditor();
+}
+
+function validateListFieldsAndExit(setting) {
+ validateListFields(setting);
+ exitEditor();
+}
+
+function assertFieldValidationError({ message, fieldLabel }) {
+ cy.contains('label', fieldLabel)
+ .siblings('ul[class*=ControlErrorsList]')
+ .contains(message);
+}
+
+module.exports = {
+ login,
+ createPost,
+ createPostAndExit,
+ updateExistingPostAndExit,
+ exitEditor,
+ goToWorkflow,
+ goToCollections,
+ updateWorkflowStatus,
+ publishWorkflowEntry,
+ deleteWorkflowEntry,
+ assertWorkflowStatusInEditor,
+ assertPublishedEntry,
+ deleteEntryInEditor,
+ assertOnCollectionsPage,
+ assertEntryDeleted,
+ assertWorkflowStatus,
+ updateWorkflowStatusInEditor,
+ validateObjectFieldsAndExit,
+ validateNestedObjectFieldsAndExit,
+ validateListFieldsAndExit,
+};
diff --git a/dev-test/backends/github/config.yml b/dev-test/backends/github/config.yml
new file mode 100644
index 00000000..08a7bac2
--- /dev/null
+++ b/dev-test/backends/github/config.yml
@@ -0,0 +1,65 @@
+backend:
+ name: github
+ branch: master
+ repo: owner/repo
+
+publish_mode: editorial_workflow
+media_folder: static/media
+public_folder: /media
+collections:
+ - name: posts
+ label: Posts
+ label_singular: 'Post'
+ folder: content/posts
+ create: true
+ slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
+ preview_path: 'posts/{{slug}}/index.html'
+ fields:
+ - label: Template
+ name: template
+ widget: hidden
+ default: post
+ - label: Title
+ name: title
+ widget: string
+ - label: Draft
+ name: draft
+ widget: boolean
+ default: true
+ - label: Publish Date
+ name: date
+ widget: datetime
+ - label: Description
+ name: description
+ widget: text
+ - label: Category
+ name: category
+ widget: string
+ - label: Body
+ name: body
+ widget: markdown
+ - label: Tags
+ name: tags
+ widget: list
+ - name: pages
+ label: Pages
+ label_singular: 'Page'
+ folder: content/pages
+ create: true
+ slug: '{{slug}}'
+ preview_path: 'pages/{{slug}}/index.html'
+ fields:
+ - label: Template
+ name: template
+ widget: hidden
+ default: page
+ - label: Title
+ name: title
+ widget: string
+ - label: Draft
+ name: draft
+ widget: boolean
+ default: true
+ - label: Body
+ name: body
+ widget: markdown
diff --git a/dev-test/backends/github/index.html b/dev-test/backends/github/index.html
new file mode 100644
index 00000000..103dbb27
--- /dev/null
+++ b/dev-test/backends/github/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Netlify CMS Development Test
+
+
+
+
+
+
diff --git a/dev-test/backends/test/config.yml b/dev-test/backends/test/config.yml
new file mode 100644
index 00000000..3da9ab1f
--- /dev/null
+++ b/dev-test/backends/test/config.yml
@@ -0,0 +1,231 @@
+backend:
+ name: test-repo
+
+site_url: https://example.com
+
+publish_mode: editorial_workflow
+media_folder: assets/uploads
+
+collections: # A list of collections the CMS should be able to edit
+ - name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
+ label: 'Posts' # Used in the UI
+ label_singular: 'Post' # Used in the UI, ie: "New Post"
+ description: >
+ The description is a great place for tone setting, high level information, and editing
+ guidelines that are specific to a collection.
+ folder: '_posts'
+ slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
+ summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
+ create: true # Allow users to create new documents in this collection
+ fields: # The fields each document in this collection have
+ - { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
+ - {
+ label: 'Publish Date',
+ name: 'date',
+ widget: 'datetime',
+ dateFormat: 'YYYY-MM-DD',
+ timeFormat: 'HH:mm',
+ format: 'YYYY-MM-DD HH:mm',
+ }
+ - label: 'Cover Image'
+ name: 'image'
+ widget: 'image'
+ required: false
+ tagname: ''
+
+ - { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
+ meta:
+ - { label: 'SEO Description', name: 'description', widget: 'text' }
+
+ - name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit
+ label: 'FAQ' # Used in the UI
+ folder: '_faqs'
+ create: true # Allow users to create new documents in this collection
+ fields: # The fields each document in this collection have
+ - { label: 'Question', name: 'title', widget: 'string', tagname: 'h1' }
+ - { label: 'Answer', name: 'body', widget: 'markdown' }
+
+ - name: 'settings'
+ label: 'Settings'
+ delete: false # Prevent users from deleting documents in this collection
+ editor:
+ preview: false
+ files:
+ - name: 'general'
+ label: 'Site Settings'
+ file: '_data/settings.json'
+ description: 'General Site Settings'
+ fields:
+ - { label: 'Global title', name: 'site_title', widget: 'string' }
+ - label: 'Post Settings'
+ name: posts
+ widget: 'object'
+ fields:
+ - {
+ label: 'Number of posts on frontpage',
+ name: front_limit,
+ widget: number,
+ min: 1,
+ max: 10,
+ }
+ - { label: 'Default Author', name: author, widget: string }
+ - { label: 'Default Thumbnail', name: thumb, widget: image, class: 'thumb', required: false }
+
+ - name: 'authors'
+ label: 'Authors'
+ file: '_data/authors.yml'
+ description: 'Author descriptions'
+ fields:
+ - name: authors
+ label: Authors
+ label_singular: 'Author'
+ widget: list
+ fields:
+ - { label: 'Name', name: 'name', widget: 'string', hint: 'First and Last' }
+ - { label: 'Description', name: 'description', widget: 'markdown' }
+
+ - name: 'kitchenSink' # all the things in one entry, for documentation and quick testing
+ label: 'Kitchen Sink'
+ folder: '_sink'
+ create: true
+ fields:
+ - label: 'Related Post'
+ name: 'post'
+ widget: 'relationKitchenSinkPost'
+ collection: 'posts'
+ displayFields: ['title', 'date']
+ searchFields: ['title', 'body']
+ valueField: 'title'
+ - { label: 'Title', name: 'title', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: true }
+ - { label: 'Map', name: 'map', widget: 'map' }
+ - { label: 'Text', name: 'text', widget: 'text', hint: 'Plain text, not markdown' }
+ - { label: 'Number', name: 'number', widget: 'number', hint: 'To infinity and beyond!' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
+ - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
+ - {
+ label: 'Select multiple',
+ name: 'select_multiple',
+ widget: 'select',
+ options: ['a', 'b', 'c'],
+ multiple: true,
+ }
+ - { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
+ - label: 'Object'
+ name: 'object'
+ widget: 'object'
+ fields:
+ - label: 'Related Post'
+ name: 'post'
+ widget: 'relationKitchenSinkPost'
+ collection: 'posts'
+ searchFields: ['title', 'body']
+ valueField: 'title'
+ - { label: 'String', name: 'string', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean', default: false }
+ - { label: 'Text', name: 'text', widget: 'text' }
+ - { label: 'Number', name: 'number', widget: 'number' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
+ - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
+ - label: 'List'
+ name: 'list'
+ widget: 'list'
+ fields:
+ - { label: 'String', name: 'string', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
+ - { label: 'Text', name: 'text', widget: 'text' }
+ - { label: 'Number', name: 'number', widget: 'number' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
+ - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
+ - label: 'Object'
+ name: 'object'
+ widget: 'object'
+ fields:
+ - { label: 'String', name: 'string', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
+ - { label: 'Text', name: 'text', widget: 'text' }
+ - { label: 'Number', name: 'number', widget: 'number' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
+ - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
+ - label: 'List'
+ name: 'list'
+ widget: 'list'
+ fields:
+ - label: 'Related Post'
+ name: 'post'
+ widget: 'relationKitchenSinkPost'
+ collection: 'posts'
+ searchFields: ['title', 'body']
+ valueField: 'title'
+ - { label: 'String', name: 'string', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
+ - { label: 'Text', name: 'text', widget: 'text' }
+ - { label: 'Number', name: 'number', widget: 'number' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
+ - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
+ - { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' }
+ - label: 'Object'
+ name: 'object'
+ widget: 'object'
+ fields:
+ - { label: 'String', name: 'string', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
+ - { label: 'Text', name: 'text', widget: 'text' }
+ - { label: 'Number', name: 'number', widget: 'number' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
+ - {
+ label: 'Select',
+ name: 'select',
+ widget: 'select',
+ options: ['a', 'b', 'c'],
+ }
+ - label: 'Typed List'
+ name: 'typed_list'
+ widget: 'list'
+ types:
+ - label: 'Type 1 Object'
+ name: 'type_1_object'
+ widget: 'object'
+ fields:
+ - { label: 'String', name: 'string', widget: 'string' }
+ - { label: 'Boolean', name: 'boolean', widget: 'boolean' }
+ - { label: 'Text', name: 'text', widget: 'text' }
+ - label: 'Type 2 Object'
+ name: 'type_2_object'
+ widget: 'object'
+ fields:
+ - { label: 'Number', name: 'number', widget: 'number' }
+ - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
+ - { label: 'Datetime', name: 'datetime', widget: 'datetime' }
+ - { label: 'Markdown', name: 'markdown', widget: 'markdown' }
+ - label: 'Type 3 Object'
+ name: 'type_3_object'
+ widget: 'object'
+ fields:
+ - { label: 'Date', name: 'date', widget: 'date' }
+ - { label: 'Image', name: 'image', widget: 'image' }
+ - { label: 'File', name: 'file', widget: 'file' }
diff --git a/dev-test/backends/test/index.html b/dev-test/backends/test/index.html
new file mode 100644
index 00000000..742b8e62
--- /dev/null
+++ b/dev-test/backends/test/index.html
@@ -0,0 +1,208 @@
+
+
+
+
+
+ Netlify CMS Development Test
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 1e975eed..960ba48f 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"@babel/plugin-proposal-object-rest-spread": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
+ "@octokit/rest": "^16.28.7",
"all-contributors-cli": "^6.0.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
@@ -89,17 +90,21 @@
"copy-webpack-plugin": "^5.0.1",
"cross-env": "^5.2.0",
"css-loader": "^3.0.0",
- "cypress": "^3.1.5",
+ "cypress": "^3.4.1",
"dom-testing-library": "^4.0.0",
+ "dotenv": "^8.0.0",
"eslint": "^5.15.1",
+ "eslint-plugin-cypress": "^2.6.0",
"eslint-plugin-emotion": "^10.0.7",
"eslint-plugin-react": "^7.12.4",
"friendly-errors-webpack-plugin": "^1.7.0",
+ "fs-extra": "^8.1.0",
"http-server": "^0.11.1",
"jest": "^24.5.0",
"jest-cli": "^24.5.0",
"jest-dom": "^3.1.3",
"jest-emotion": "^10.0.9",
+ "js-yaml": "^3.13.1",
"ncp": "^2.0.0",
"nock": "^10.0.4",
"node-fetch": "^2.3.0",
@@ -108,6 +113,7 @@
"react-test-renderer": "^16.8.4",
"react-testing-library": "^7.0.0",
"rimraf": "^2.6.3",
+ "simple-git": "^1.124.0",
"start-server-and-test": "^1.7.11",
"stylelint": "^9.10.1",
"stylelint-config-recommended": "^2.1.0",
diff --git a/packages/netlify-cms-backend-github/package.json b/packages/netlify-cms-backend-github/package.json
index 4a71c87d..c11252ff 100644
--- a/packages/netlify-cms-backend-github/package.json
+++ b/packages/netlify-cms-backend-github/package.json
@@ -17,10 +17,17 @@
"scripts": {
"develop": "yarn build:esm --watch",
"build": "cross-env NODE_ENV=production webpack",
- "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward"
+ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore **/__tests__ --root-mode upward",
+ "createFragmentTypes": "node scripts/createFragmentTypes.js"
},
"dependencies": {
+ "apollo-cache-inmemory": "^1.6.2",
+ "apollo-client": "^2.6.3",
+ "apollo-link-context": "^1.0.18",
+ "apollo-link-http": "^1.5.15",
"common-tags": "^1.8.0",
+ "graphql": "^14.4.2",
+ "graphql-tag": "^2.10.1",
"js-base64": "^2.5.1",
"semaphore": "^1.1.0"
},
diff --git a/packages/netlify-cms-backend-github/scripts/createFragmentTypes.js b/packages/netlify-cms-backend-github/scripts/createFragmentTypes.js
new file mode 100644
index 00000000..c60835a9
--- /dev/null
+++ b/packages/netlify-cms-backend-github/scripts/createFragmentTypes.js
@@ -0,0 +1,48 @@
+const fetch = require('node-fetch');
+const fs = require('fs');
+const path = require('path');
+
+const API_HOST = process.env.GITHUB_HOST || 'https://api.github.com';
+const API_TOKEN = process.env.GITHUB_API_TOKEN;
+
+if (!API_TOKEN) {
+ throw new Error('Missing environment variable GITHUB_API_TOKEN');
+}
+
+fetch(`${API_HOST}/graphql`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: `bearer ${API_TOKEN}` },
+ body: JSON.stringify({
+ variables: {},
+ query: `
+ {
+ __schema {
+ types {
+ kind
+ name
+ possibleTypes {
+ name
+ }
+ }
+ }
+ }
+ `,
+ }),
+})
+ .then(result => result.json())
+ .then(result => {
+ // here we're filtering out any type information unrelated to unions or interfaces
+ const filteredData = result.data.__schema.types.filter(type => type.possibleTypes !== null);
+ result.data.__schema.types = filteredData;
+ fs.writeFile(
+ path.join(__dirname, '..', 'src', 'fragmentTypes.js'),
+ `module.exports = ${JSON.stringify(result.data)}`,
+ err => {
+ if (err) {
+ console.error('Error writing fragmentTypes file', err);
+ } else {
+ console.log('Fragment types successfully extracted!');
+ }
+ },
+ );
+ });
diff --git a/packages/netlify-cms-backend-github/src/API.js b/packages/netlify-cms-backend-github/src/API.js
index 2353161e..9815ad33 100644
--- a/packages/netlify-cms-backend-github/src/API.js
+++ b/packages/netlify-cms-backend-github/src/API.js
@@ -3,7 +3,7 @@ import semaphore from 'semaphore';
import { find, flow, get, hasIn, initial, last, partial, result, uniq } from 'lodash';
import { map } from 'lodash/fp';
import {
- getPaginatedRequestIterator,
+ getAllResponses,
APIError,
EditorialWorkflowError,
filterPromisesWith,
@@ -21,15 +21,19 @@ export default class API {
this.api_root = config.api_root || 'https://api.github.com';
this.token = config.token || false;
this.branch = config.branch || 'master';
- this.originRepo = config.originRepo;
this.useOpenAuthoring = config.useOpenAuthoring;
this.repo = config.repo || '';
+ this.originRepo = config.originRepo || this.repo;
this.repoURL = `/repos/${this.repo}`;
- this.originRepoURL = this.originRepo && `/repos/${this.originRepo}`;
+ // when not in 'useOpenAuthoring' mode originRepoURL === repoURL
+ this.originRepoURL = `/repos/${this.originRepo}`;
this.merge_method = config.squash_merges ? 'squash' : 'merge';
this.initialWorkflowStatus = config.initialWorkflowStatus;
}
+ static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Netlify CMS';
+ static DEFAULT_PR_BODY = 'Automatically generated by Netlify CMS';
+
user() {
if (!this._userPromise) {
this._userPromise = this.request('/user');
@@ -113,13 +117,10 @@ export default class API {
}
async requestAllPages(url, options = {}) {
+ const headers = this.requestHeaders(options.headers || {});
const processedURL = this.urlFor(url, options);
- const pagesIterator = getPaginatedRequestIterator(processedURL, options);
- const pagesToParse = [];
- for await (const page of pagesIterator) {
- pagesToParse.push(this.parseResponse(page));
- }
- const pages = await Promise.all(pagesToParse);
+ const allResponses = await getAllResponses(processedURL, { ...options, headers });
+ const pages = await Promise.all(allResponses.map(res => this.parseResponse(res)));
return [].concat(...pages);
}
@@ -224,34 +225,46 @@ export default class API {
cache: 'no-store',
};
+ const errorHandler = err => {
+ if (err.message === 'Not Found') {
+ console.log(
+ '%c %s does not have metadata',
+ 'line-height: 30px;text-align: center;font-weight: bold',
+ key,
+ );
+ }
+ throw err;
+ };
+
if (!this.useOpenAuthoring) {
return this.request(`${this.repoURL}/contents/${key}.json`, metadataRequestOptions)
.then(response => JSON.parse(response))
- .catch(err => {
- if (err.message === 'Not Found') {
- console.log(
- '%c %s does not have metadata',
- 'line-height: 30px;text-align: center;font-weight: bold',
- key,
- );
- }
- throw err;
- });
+ .catch(errorHandler);
}
const [user, repo] = key.split('/');
return this.request(`/repos/${user}/${repo}/contents/${key}.json`, metadataRequestOptions)
.then(response => JSON.parse(response))
- .catch(err => {
- if (err.message === 'Not Found') {
- console.log(
- '%c %s does not have metadata',
- 'line-height: 30px;text-align: center;font-weight: bold',
- key,
- );
- }
- throw err;
- });
+ .catch(errorHandler);
+ });
+ }
+
+ retrieveContent(path, branch, repoURL) {
+ return this.request(`${repoURL}/contents/${path}`, {
+ headers: { Accept: 'application/vnd.github.VERSION.raw' },
+ params: { ref: branch },
+ cache: 'no-store',
+ }).catch(error => {
+ if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) {
+ const dir = path
+ .split('/')
+ .slice(0, -1)
+ .join('/');
+ return this.listFiles(dir, { repoURL, branch })
+ .then(files => files.find(file => file.path === path))
+ .then(file => this.getBlob(file.sha, { repoURL }));
+ }
+ throw error;
});
}
@@ -259,34 +272,23 @@ export default class API {
if (sha) {
return this.getBlob(sha);
} else {
- return this.request(`${repoURL}/contents/${path}`, {
- headers: { Accept: 'application/vnd.github.VERSION.raw' },
- params: { ref: branch },
- cache: 'no-store',
- }).catch(error => {
- if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) {
- const dir = path
- .split('/')
- .slice(0, -1)
- .join('/');
- return this.listFiles(dir, { repoURL, branch })
- .then(files => files.find(file => file.path === path))
- .then(file => this.getBlob(file.sha, { repoURL }));
- }
- throw error;
- });
+ return this.retrieveContent(path, branch, repoURL);
}
}
+ retrieveBlob(sha, repoURL) {
+ return this.request(`${repoURL}/git/blobs/${sha}`, {
+ headers: { Accept: 'application/vnd.github.VERSION.raw' },
+ });
+ }
+
getBlob(sha, { repoURL = this.repoURL } = {}) {
return localForage.getItem(`gh.${sha}`).then(cached => {
if (cached) {
return cached;
}
- return this.request(`${repoURL}/git/blobs/${sha}`, {
- headers: { Accept: 'application/vnd.github.VERSION.raw' },
- }).then(result => {
+ return this.retrieveBlob(sha, repoURL).then(result => {
localForage.setItem(`gh.${sha}`, result);
return result;
});
@@ -335,7 +337,7 @@ export default class API {
isUnpublishedEntryModification(path, branch) {
return this.readFile(path, null, {
branch,
- repoURL: this.useOpenAuthoring ? this.originRepoURL : this.repoURL,
+ repoURL: this.originRepoURL,
})
.then(() => true)
.catch(err => {
@@ -385,7 +387,7 @@ export default class API {
// closed or not and update the status accordingly.
if (prMetadata) {
const { number: prNumber } = prMetadata;
- const originPRInfo = await this.request(`${this.originRepoURL}/pulls/${prNumber}`);
+ const originPRInfo = await this.getPullRequest(prNumber);
const { state: currentState, merged_at: mergedAt } = originPRInfo;
if (currentState === 'closed' && mergedAt) {
// The PR has been merged; delete the unpublished entry
@@ -453,9 +455,8 @@ export default class API {
* concept of entry "status". Useful for things like deploy preview links.
*/
async getStatuses(sha) {
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
try {
- const resp = await this.request(`${repoURL}/commits/${sha}/status`);
+ const resp = await this.request(`${this.originRepoURL}/commits/${sha}/status`);
return resp.statuses;
} catch (err) {
if (err && err.message && err.message === 'Ref not found') {
@@ -517,25 +518,33 @@ export default class API {
});
}
- deleteFile(path, message, options = {}) {
- if (this.useOpenAuthoring) {
- return Promise.reject('Cannot delete published entries as an Open Authoring user!');
- }
- const branch = options.branch || this.branch;
- const pathArray = path.split('/');
- const filename = last(pathArray);
- const directory = initial(pathArray).join('/');
- const fileDataPath = encodeURIComponent(directory);
- const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`;
- const fileURL = `${this.repoURL}/contents/${path}`;
-
+ getFileSha(path, branch) {
/**
* We need to request the tree first to get the SHA. We use extended SHA-1
* syntax (:) to get a blob from a tree without having to recurse
* through the tree.
*/
+
+ const pathArray = path.split('/');
+ const filename = last(pathArray);
+ const directory = initial(pathArray).join('/');
+ const fileDataPath = encodeURIComponent(directory);
+ const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`;
+
return this.request(fileDataURL, { cache: 'no-store' }).then(resp => {
const { sha } = resp.tree.find(file => file.path === filename);
+ return sha;
+ });
+ }
+
+ deleteFile(path, message, options = {}) {
+ if (this.useOpenAuthoring) {
+ return Promise.reject('Cannot delete published entries as an Open Authoring user!');
+ }
+
+ const branch = options.branch || this.branch;
+
+ return this.getFileSha(path, branch).then(sha => {
const opts = { method: 'DELETE', params: { sha, message, branch } };
if (this.commitAuthor) {
opts.params.author = {
@@ -543,10 +552,16 @@ export default class API {
date: new Date().toISOString(),
};
}
+ const fileURL = `${this.repoURL}/contents/${path}`;
return this.request(fileURL, opts);
});
}
+ async createBranchAndPullRequest(branchName, sha, commitMessage) {
+ await this.createBranch(branchName, sha);
+ return this.createPR(commitMessage, branchName);
+ }
+
async editorialWorkflowGit(fileTree, entry, filesList, options) {
const contentKey = this.generateContentKey(options.collectionName, entry.slug);
const branchName = this.generateBranchName(contentKey);
@@ -557,10 +572,18 @@ export default class API {
const branchData = await this.getBranch();
const changeTree = await this.updateTree(branchData.commit.sha, '/', fileTree);
const commitResponse = await this.commit(options.commitMessage, changeTree);
- await this.createBranch(branchName, commitResponse.sha);
- const pr = this.useOpenAuthoring
- ? undefined
- : await this.createPR(options.commitMessage, branchName);
+
+ let pr;
+ if (this.useOpenAuthoring) {
+ await this.createBranch(branchName, commitResponse.sha);
+ } else {
+ pr = await this.createBranchAndPullRequest(
+ branchName,
+ commitResponse.sha,
+ options.commitMessage,
+ );
+ }
+
const user = await userPromise;
return this.storeMetadata(contentKey, {
type: 'PR',
@@ -610,6 +633,9 @@ export default class API {
if (pr) {
return this.rebasePullRequest(pr.number, branchName, contentKey, metadata, commit);
+ } else if (this.useOpenAuthoring) {
+ // if a PR hasn't been created yet for the forked repo, just patch the branch
+ await this.patchBranch(branchName, commit.sha, { force: true });
}
return this.storeMetadata(contentKey, updatedMetadata);
@@ -629,8 +655,10 @@ export default class API {
* Get the published branch and create new commits over it. If the pull
* request is up to date, no rebase will occur.
*/
- const baseBranch = await this.getBranch();
- const commits = await this.getPullRequestCommits(prNumber, head);
+ const [baseBranch, commits] = await Promise.all([
+ this.getBranch(),
+ this.getPullRequestCommits(prNumber, head),
+ ]);
/**
* Sometimes the list of commits for a pull request isn't updated
@@ -731,16 +759,14 @@ export default class API {
* Get a pull request by PR number.
*/
getPullRequest(prNumber) {
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
- return this.request(`${repoURL}/pulls/${prNumber} }`);
+ return this.request(`${this.originRepoURL}/pulls/${prNumber} }`);
}
/**
* Get the list of commits for a given pull request.
*/
getPullRequestCommits(prNumber) {
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
- return this.request(`${repoURL}/pulls/${prNumber}/commits`);
+ return this.request(`${this.originRepoURL}/pulls/${prNumber}/commits`);
}
/**
@@ -779,7 +805,7 @@ export default class API {
const { pr: prMetadata } = metadata;
if (prMetadata) {
const { number: prNumber } = prMetadata;
- const originPRInfo = await this.request(`${this.originRepoURL}/pulls/${prNumber}`);
+ const originPRInfo = await this.getPullRequest(prNumber);
const { state } = originPRInfo;
if (state === 'open' && status === 'draft') {
await this.closePR(prMetadata);
@@ -800,7 +826,7 @@ export default class API {
if (!prMetadata && status === 'pending_review') {
const branchName = this.generateBranchName(contentKey);
- const commitMessage = metadata.commitMessage || 'Automatically generated by Netlify CMS';
+ const commitMessage = metadata.commitMessage || API.DEFAULT_COMMIT_MESSAGE;
const { number, head } = await this.createPR(commitMessage, branchName);
return this.storeMetadata(contentKey, {
...metadata,
@@ -883,21 +909,23 @@ export default class API {
return this.deleteRef('heads', branchName);
}
- async createPR(title, head, base = this.branch) {
- const body = 'Automatically generated by Netlify CMS';
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
+ async createPR(title, head) {
const headReference = this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head;
- return this.request(`${repoURL}/pulls`, {
+ return this.request(`${this.originRepoURL}/pulls`, {
method: 'POST',
- body: JSON.stringify({ title, body, head: headReference, base }),
+ body: JSON.stringify({
+ title,
+ body: API.DEFAULT_PR_BODY,
+ head: headReference,
+ base: this.branch,
+ }),
});
}
async openPR(pullRequest) {
const { number } = pullRequest;
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
console.log('%c Re-opening PR', 'line-height: 30px;text-align: center;font-weight: bold');
- return this.request(`${repoURL}/pulls/${number}`, {
+ return this.request(`${this.originRepoURL}/pulls/${number}`, {
method: 'PATCH',
body: JSON.stringify({
state: 'open',
@@ -905,11 +933,10 @@ export default class API {
});
}
- closePR(pullrequest) {
- const prNumber = pullrequest.number;
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
+ closePR(pullRequest) {
+ const { number } = pullRequest;
console.log('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold');
- return this.request(`${repoURL}/pulls/${prNumber}`, {
+ return this.request(`${this.originRepoURL}/pulls/${number}`, {
method: 'PATCH',
body: JSON.stringify({
state: 'closed',
@@ -918,11 +945,9 @@ export default class API {
}
mergePR(pullrequest, objects) {
- const headSha = pullrequest.head;
- const prNumber = pullrequest.number;
- const repoURL = this.useOpenAuthoring ? this.originRepoURL : this.repoURL;
+ const { head: headSha, number } = pullrequest;
console.log('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold');
- return this.request(`${repoURL}/pulls/${prNumber}/merge`, {
+ return this.request(`${this.originRepoURL}/pulls/${number}/merge`, {
method: 'PUT',
body: JSON.stringify({
commit_message: 'Automatically generated. Merged on Netlify CMS.',
diff --git a/packages/netlify-cms-backend-github/src/GraphQLAPI.js b/packages/netlify-cms-backend-github/src/GraphQLAPI.js
new file mode 100644
index 00000000..d324f7d9
--- /dev/null
+++ b/packages/netlify-cms-backend-github/src/GraphQLAPI.js
@@ -0,0 +1,627 @@
+import { ApolloClient } from 'apollo-client';
+import {
+ InMemoryCache,
+ defaultDataIdFromObject,
+ IntrospectionFragmentMatcher,
+} from 'apollo-cache-inmemory';
+import { createHttpLink } from 'apollo-link-http';
+import { setContext } from 'apollo-link-context';
+import { APIError, EditorialWorkflowError } from 'netlify-cms-lib-util';
+import introspectionQueryResultData from './fragmentTypes';
+import API from './API';
+import * as queries from './queries';
+import * as mutations from './mutations';
+
+const NO_CACHE = 'no-cache';
+const CACHE_FIRST = 'cache-first';
+
+const TREE_ENTRY_TYPE_TO_MODE = {
+ blob: '100644',
+ tree: '040000',
+ commit: '160000',
+};
+
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
+
+export default class GraphQLAPI extends API {
+ constructor(config) {
+ super(config);
+
+ const [repoParts, originRepoParts] = [this.repo.split('/'), this.originRepo.split('/')];
+ this.repo_owner = repoParts[0];
+ this.repo_name = repoParts[1];
+
+ this.origin_repo_owner = originRepoParts[0];
+ this.origin_repo_name = originRepoParts[1];
+
+ this.client = this.getApolloClient();
+ }
+
+ getApolloClient() {
+ const authLink = setContext((_, { headers }) => {
+ return {
+ headers: {
+ ...headers,
+ authorization: this.token ? `token ${this.token}` : '',
+ },
+ };
+ });
+ const httpLink = createHttpLink({ uri: `${this.api_root}/graphql` });
+ return new ApolloClient({
+ link: authLink.concat(httpLink),
+ cache: new InMemoryCache({ fragmentMatcher }),
+ defaultOptions: {
+ watchQuery: {
+ fetchPolicy: NO_CACHE,
+ errorPolicy: 'ignore',
+ },
+ query: {
+ fetchPolicy: NO_CACHE,
+ errorPolicy: 'all',
+ },
+ },
+ });
+ }
+
+ reset() {
+ return this.client.resetStore();
+ }
+
+ async getRepository(owner, name) {
+ const { data } = await this.query({
+ query: queries.repository,
+ variables: { owner, name },
+ fetchPolicy: CACHE_FIRST, // repository id doesn't change
+ });
+ return data.repository;
+ }
+
+ query(options = {}) {
+ return this.client.query(options).catch(error => {
+ throw new APIError(error.message, 500, 'GitHub');
+ });
+ }
+
+ mutate(options = {}) {
+ return this.client.mutate(options).catch(error => {
+ throw new APIError(error.message, 500, 'GitHub');
+ });
+ }
+
+ async hasWriteAccess() {
+ const { repo_owner: owner, repo_name: name } = this;
+ try {
+ const { data } = await this.query({
+ query: queries.repoPermission,
+ variables: { owner, name },
+ fetchPolicy: CACHE_FIRST, // we can assume permission doesn't change often
+ });
+ // https://developer.github.com/v4/enum/repositorypermission/
+ const { viewerPermission } = data.repository;
+ return ['ADMIN', 'MAINTAIN', 'WRITE'].includes(viewerPermission);
+ } catch (error) {
+ console.error('Problem fetching repo data from GitHub');
+ throw error;
+ }
+ }
+
+ async user() {
+ const { data } = await this.query({
+ query: queries.user,
+ fetchPolicy: CACHE_FIRST, // we can assume user details don't change often
+ });
+ return data.viewer;
+ }
+
+ async retrieveBlobObject(owner, name, expression, options = {}) {
+ const { data } = await this.query({
+ query: queries.blob,
+ variables: { owner, name, expression },
+ ...options,
+ });
+ // https://developer.github.com/v4/object/blob/
+ if (data.repository.object) {
+ const { is_binary, text } = data.repository.object;
+ return { is_null: false, is_binary, text };
+ } else {
+ return { is_null: true };
+ }
+ }
+
+ getOwnerAndNameFromRepoUrl(repoURL) {
+ let { repo_owner: owner, repo_name: name } = this;
+
+ if (repoURL === this.originRepoURL) {
+ ({ origin_repo_owner: owner, origin_repo_name: name } = this);
+ }
+
+ return { owner, name };
+ }
+
+ async retrieveContent(path, branch, repoURL) {
+ const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
+ const { is_null, is_binary, text } = await this.retrieveBlobObject(
+ owner,
+ name,
+ `${branch}:${path}`,
+ );
+ if (is_null) {
+ throw new APIError('Not Found', 404, 'GitHub');
+ } else if (!is_binary) {
+ return text;
+ } else {
+ return super.retrieveContent(path, branch, repoURL);
+ }
+ }
+
+ async retrieveBlob(sha, repoURL) {
+ const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL);
+ const { is_null, is_binary, text } = await this.retrieveBlobObject(
+ owner,
+ name,
+ sha,
+ { fetchPolicy: CACHE_FIRST }, // blob sha is derived from file content
+ );
+
+ if (is_null) {
+ throw new APIError('Not Found', 404, 'GitHub');
+ } else if (!is_binary) {
+ return text;
+ } else {
+ return super.retrieveBlob(sha);
+ }
+ }
+
+ async getStatuses(sha) {
+ const { origin_repo_owner: owner, origin_repo_name: name } = this;
+ const { data } = await this.query({ query: queries.statues, variables: { owner, name, sha } });
+ if (data.repository.object) {
+ const { status } = data.repository.object;
+ const { contexts } = status || { contexts: [] };
+ return contexts;
+ } else {
+ return [];
+ }
+ }
+
+ async listFiles(path) {
+ const { repo_owner: owner, repo_name: name } = this;
+ const { data } = await this.query({
+ query: queries.files,
+ variables: { owner, name, expression: `${this.branch}:${path}` },
+ });
+
+ if (data.repository.object) {
+ const files = data.repository.object.entries.map(e => ({
+ ...e,
+ path: `${path}/${e.name}`,
+ download_url: `https://raw.githubusercontent.com/${this.repo}/${this.branch}/${path}/${e.name}`,
+ size: e.blob && e.blob.size,
+ }));
+ return files;
+ } else {
+ throw new APIError('Not Found', 404, 'GitHub');
+ }
+ }
+
+ async listUnpublishedBranches() {
+ if (this.useOpenAuthoring) {
+ return super.listUnpublishedBranches();
+ }
+ console.log(
+ '%c Checking for Unpublished entries',
+ 'line-height: 30px;text-align: center;font-weight: bold',
+ );
+ const { repo_owner: owner, repo_name: name } = this;
+ const { data } = await this.query({
+ query: queries.unpublishedPrBranches,
+ variables: { owner, name },
+ });
+ const { nodes } = data.repository.refs;
+ if (nodes.length > 0) {
+ const branches = [];
+ nodes.forEach(({ associatedPullRequests }) => {
+ associatedPullRequests.nodes.forEach(({ headRef }) => {
+ branches.push({ ref: `${headRef.prefix}${headRef.name}` });
+ });
+ });
+ return branches;
+ } else {
+ console.log(
+ '%c No Unpublished entries',
+ 'line-height: 30px;text-align: center;font-weight: bold',
+ );
+ throw new APIError('Not Found', 404, 'GitHub');
+ }
+ }
+
+ async readUnpublishedBranchFile(contentKey) {
+ // retrieveMetadata(contentKey) rejects in case of no metadata
+ const metaData = await this.retrieveMetadata(contentKey).catch(() => null);
+ if (metaData && metaData.objects && metaData.objects.entry && metaData.objects.entry.path) {
+ const { path } = metaData.objects.entry;
+ const { repo_owner: headOwner, repo_name: headRepoName } = this;
+ const { origin_repo_owner: baseOwner, origin_repo_name: baseRepoName } = this;
+
+ const { data } = await this.query({
+ query: queries.unpublishedBranchFile,
+ variables: {
+ headOwner,
+ headRepoName,
+ headExpression: `${metaData.branch}:${path}`,
+ baseOwner,
+ baseRepoName,
+ baseExpression: `${this.branch}:${path}`,
+ },
+ });
+ if (!data.head.object) {
+ throw new EditorialWorkflowError('content is not under editorial workflow', true);
+ }
+ const result = {
+ metaData,
+ fileData: data.head.object.text,
+ isModification: !!data.base.object,
+ };
+ return result;
+ } else {
+ throw new EditorialWorkflowError('content is not under editorial workflow', true);
+ }
+ }
+
+ getBranchQualifiedName(branch) {
+ return `refs/heads/${branch}`;
+ }
+
+ getBranchQuery(branch) {
+ const { repo_owner: owner, repo_name: name } = this;
+
+ return {
+ query: queries.branch,
+ variables: {
+ owner,
+ name,
+ qualifiedName: this.getBranchQualifiedName(branch),
+ },
+ };
+ }
+
+ async getBranch(branch = this.branch) {
+ // don't cache base branch to always get the latest data
+ const fetchPolicy = branch === this.branch ? NO_CACHE : CACHE_FIRST;
+ const { data } = await this.query({
+ ...this.getBranchQuery(branch),
+ fetchPolicy,
+ });
+ return data.repository.branch;
+ }
+
+ async patchRef(type, name, sha, opts = {}) {
+ if (type !== 'heads') {
+ return super.patchRef(type, name, sha, opts);
+ }
+
+ const force = opts.force || false;
+
+ const branch = await this.getBranch(name);
+ const { data } = await this.mutate({
+ mutation: mutations.updateBranch,
+ variables: {
+ input: { oid: sha, refId: branch.id, force },
+ },
+ });
+ return data.updateRef.branch;
+ }
+
+ async deleteBranch(branchName) {
+ const branch = await this.getBranch(branchName);
+ const { data } = await this.mutate({
+ mutation: mutations.deleteBranch,
+ variables: {
+ deleteRefInput: { refId: branch.id },
+ },
+ update: store => store.data.delete(defaultDataIdFromObject(branch)),
+ });
+
+ return data.deleteRef;
+ }
+
+ getPullRequestQuery(number) {
+ const { origin_repo_owner: owner, origin_repo_name: name } = this;
+
+ return {
+ query: queries.pullRequest,
+ variables: { owner, name, number },
+ };
+ }
+
+ async getPullRequest(number) {
+ const { data } = await this.query({
+ ...this.getPullRequestQuery(number),
+ fetchPolicy: CACHE_FIRST,
+ });
+
+ // https://developer.github.com/v4/enum/pullrequeststate/
+ // GraphQL state: [CLOSED, MERGED, OPEN]
+ // REST API state: [closed, open]
+ const state = data.repository.pullRequest.state === 'OPEN' ? 'open' : 'closed';
+ return {
+ ...data.repository.pullRequest,
+ state,
+ };
+ }
+
+ getPullRequestAndBranchQuery(branch, number) {
+ const { repo_owner: owner, repo_name: name } = this;
+ const { origin_repo_owner: origin_owner, origin_repo_name: origin_name } = this;
+
+ return {
+ query: queries.pullRequestAndBranch,
+ variables: {
+ owner,
+ name,
+ origin_owner,
+ origin_name,
+ number,
+ qualifiedName: this.getBranchQualifiedName(branch),
+ },
+ };
+ }
+
+ async getPullRequestAndBranch(branch, number) {
+ const { data } = await this.query({
+ ...this.getPullRequestAndBranchQuery(branch, number),
+ fetchPolicy: CACHE_FIRST,
+ });
+
+ const { repository, origin } = data;
+ return { branch: repository.branch, pullRequest: origin.pullRequest };
+ }
+
+ async openPR({ number }) {
+ const pullRequest = await this.getPullRequest(number);
+
+ const { data } = await this.mutate({
+ mutation: mutations.reopenPullRequest,
+ variables: {
+ reopenPullRequestInput: { pullRequestId: pullRequest.id },
+ },
+ update: (store, { data: mutationResult }) => {
+ const { pullRequest } = mutationResult.reopenPullRequest;
+ const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
+
+ store.writeQuery({
+ ...this.getPullRequestQuery(pullRequest.number),
+ data: pullRequestData,
+ });
+ },
+ });
+
+ return data.closePullRequest;
+ }
+
+ async closePR({ number }) {
+ const pullRequest = await this.getPullRequest(number);
+
+ const { data } = await this.mutate({
+ mutation: mutations.closePullRequest,
+ variables: {
+ closePullRequestInput: { pullRequestId: pullRequest.id },
+ },
+ update: (store, { data: mutationResult }) => {
+ const { pullRequest } = mutationResult.closePullRequest;
+ const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
+
+ store.writeQuery({
+ ...this.getPullRequestQuery(pullRequest.number),
+ data: pullRequestData,
+ });
+ },
+ });
+
+ return data.closePullRequest;
+ }
+
+ async deleteUnpublishedEntry(collectionName, slug) {
+ try {
+ const contentKey = this.generateContentKey(collectionName, slug);
+ const branchName = this.generateBranchName(contentKey);
+
+ const metadata = await this.retrieveMetadata(contentKey);
+ if (metadata && metadata.pr) {
+ const { branch, pullRequest } = await this.getPullRequestAndBranch(
+ branchName,
+ metadata.pr.number,
+ );
+
+ const { data } = await this.mutate({
+ mutation: mutations.closePullRequestAndDeleteBranch,
+ variables: {
+ deleteRefInput: { refId: branch.id },
+ closePullRequestInput: { pullRequestId: pullRequest.id },
+ },
+ update: store => {
+ store.data.delete(defaultDataIdFromObject(branch));
+ store.data.delete(defaultDataIdFromObject(pullRequest));
+ },
+ });
+
+ return data.closePullRequest;
+ } else {
+ return await this.deleteBranch(branchName);
+ }
+ } catch (e) {
+ const { graphQLErrors } = e;
+ if (graphQLErrors && graphQLErrors.length > 0) {
+ const branchNotFound = graphQLErrors.some(e => e.type === 'NOT_FOUND');
+ if (branchNotFound) {
+ return;
+ }
+ }
+ throw e;
+ }
+ }
+
+ async createPR(title, head) {
+ const [repository, headReference] = await Promise.all([
+ this.getRepository(this.origin_repo_owner, this.origin_repo_name),
+ this.useOpenAuthoring ? `${(await this.user()).login}:${head}` : head,
+ ]);
+ const { data } = await this.mutate({
+ mutation: mutations.createPullRequest,
+ variables: {
+ createPullRequestInput: {
+ baseRefName: this.branch,
+ body: API.DEFAULT_PR_BODY,
+ title,
+ headRefName: headReference,
+ repositoryId: repository.id,
+ },
+ },
+ update: (store, { data: mutationResult }) => {
+ const { pullRequest } = mutationResult.createPullRequest;
+ const pullRequestData = { repository: { ...pullRequest.repository, pullRequest } };
+
+ store.writeQuery({
+ ...this.getPullRequestQuery(pullRequest.number),
+ data: pullRequestData,
+ });
+ },
+ });
+ const { pullRequest } = data.createPullRequest;
+ return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
+ }
+
+ async createBranch(branchName, sha) {
+ const repository = await this.getRepository(this.repo_owner, this.repo_name);
+ const { data } = await this.mutate({
+ mutation: mutations.createBranch,
+ variables: {
+ createRefInput: {
+ name: this.getBranchQualifiedName(branchName),
+ oid: sha,
+ repositoryId: repository.id,
+ },
+ },
+ update: (store, { data: mutationResult }) => {
+ const { branch } = mutationResult.createRef;
+ const branchData = { repository: { ...branch.repository, branch } };
+
+ store.writeQuery({
+ ...this.getBranchQuery(branchName),
+ data: branchData,
+ });
+ },
+ });
+ const { branch } = data.createRef;
+ return branch;
+ }
+
+ async createBranchAndPullRequest(branchName, sha, title) {
+ const repository = await this.getRepository(this.origin_repo_owner, this.origin_repo_name);
+ const { data } = await this.mutate({
+ mutation: mutations.createBranchAndPullRequest,
+ variables: {
+ createRefInput: {
+ name: this.getBranchQualifiedName(branchName),
+ oid: sha,
+ repositoryId: repository.id,
+ },
+ createPullRequestInput: {
+ baseRefName: this.branch,
+ body: API.DEFAULT_PR_BODY,
+ title,
+ headRefName: branchName,
+ repositoryId: repository.id,
+ },
+ },
+ update: (store, { data: mutationResult }) => {
+ const { branch } = mutationResult.createRef;
+ const { pullRequest } = mutationResult.createPullRequest;
+ const branchData = { repository: { ...branch.repository, branch } };
+ const pullRequestData = {
+ repository: { ...pullRequest.repository, branch },
+ origin: { ...pullRequest.repository, pullRequest },
+ };
+
+ store.writeQuery({
+ ...this.getBranchQuery(branchName),
+ data: branchData,
+ });
+
+ store.writeQuery({
+ ...this.getPullRequestAndBranchQuery(branchName, pullRequest.number),
+ data: pullRequestData,
+ });
+ },
+ });
+ const { pullRequest } = data.createPullRequest;
+ return { ...pullRequest, head: { sha: pullRequest.headRefOid } };
+ }
+
+ async getTree(sha) {
+ if (!sha) {
+ return Promise.resolve({ tree: [] });
+ }
+
+ const { repo_owner: owner, repo_name: name } = this;
+ const variables = {
+ owner,
+ name,
+ sha,
+ };
+
+ // sha can be either for a commit or a tree
+ const [commitTree, tree] = await Promise.all([
+ this.client.query({
+ query: queries.commitTree,
+ variables,
+ fetchPolicy: CACHE_FIRST,
+ }),
+ this.client.query({
+ query: queries.tree,
+ variables,
+ fetchPolicy: CACHE_FIRST,
+ }),
+ ]);
+
+ let entries = null;
+
+ if (commitTree.data.repository.commit.tree) {
+ entries = commitTree.data.repository.commit.tree.entries;
+ }
+
+ if (tree.data.repository.tree.entries) {
+ entries = tree.data.repository.tree.entries;
+ }
+
+ if (entries) {
+ return { tree: entries.map(e => ({ ...e, mode: TREE_ENTRY_TYPE_TO_MODE[e.type] })) };
+ }
+
+ return Promise.reject('Could not get tree');
+ }
+
+ async getPullRequestCommits(number) {
+ const { origin_repo_owner: owner, origin_repo_name: name } = this;
+ const { data } = await this.query({
+ query: queries.pullRequestCommits,
+ variables: { owner, name, number },
+ });
+ const { nodes } = data.repository.pullRequest.commits;
+ const commits = nodes.map(n => ({ ...n.commit, parents: n.commit.parents.nodes }));
+
+ return commits;
+ }
+
+ async getFileSha(path, branch) {
+ const { repo_owner: owner, repo_name: name } = this;
+ const { data } = await this.query({
+ query: queries.fileSha,
+ variables: { owner, name, expression: `${branch}:${path}` },
+ });
+
+ return data.repository.file.sha;
+ }
+}
diff --git a/packages/netlify-cms-backend-github/src/fragmentTypes.js b/packages/netlify-cms-backend-github/src/fragmentTypes.js
new file mode 100644
index 00000000..2a654d96
--- /dev/null
+++ b/packages/netlify-cms-backend-github/src/fragmentTypes.js
@@ -0,0 +1 @@
+module.exports = {"__schema":{"types":[{"kind":"INTERFACE","name":"Node","possibleTypes":[{"name":"AddedToProjectEvent"},{"name":"App"},{"name":"AssignedEvent"},{"name":"BaseRefChangedEvent"},{"name":"BaseRefForcePushedEvent"},{"name":"Blob"},{"name":"Bot"},{"name":"BranchProtectionRule"},{"name":"ClosedEvent"},{"name":"CodeOfConduct"},{"name":"CommentDeletedEvent"},{"name":"Commit"},{"name":"CommitComment"},{"name":"CommitCommentThread"},{"name":"ConvertedNoteToIssueEvent"},{"name":"CrossReferencedEvent"},{"name":"DemilestonedEvent"},{"name":"DeployKey"},{"name":"DeployedEvent"},{"name":"Deployment"},{"name":"DeploymentEnvironmentChangedEvent"},{"name":"DeploymentStatus"},{"name":"ExternalIdentity"},{"name":"Gist"},{"name":"GistComment"},{"name":"HeadRefDeletedEvent"},{"name":"HeadRefForcePushedEvent"},{"name":"HeadRefRestoredEvent"},{"name":"Issue"},{"name":"IssueComment"},{"name":"Label"},{"name":"LabeledEvent"},{"name":"Language"},{"name":"License"},{"name":"LockedEvent"},{"name":"Mannequin"},{"name":"MarketplaceCategory"},{"name":"MarketplaceListing"},{"name":"MentionedEvent"},{"name":"MergedEvent"},{"name":"Milestone"},{"name":"MilestonedEvent"},{"name":"MovedColumnsInProjectEvent"},{"name":"Organization"},{"name":"OrganizationIdentityProvider"},{"name":"OrganizationInvitation"},{"name":"PinnedEvent"},{"name":"Project"},{"name":"ProjectCard"},{"name":"ProjectColumn"},{"name":"PublicKey"},{"name":"PullRequest"},{"name":"PullRequestCommit"},{"name":"PullRequestCommitCommentThread"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"},{"name":"PullRequestReviewThread"},{"name":"PushAllowance"},{"name":"Reaction"},{"name":"ReadyForReviewEvent"},{"name":"Ref"},{"name":"ReferencedEvent"},{"name":"RegistryPackage"},{"name":"RegistryPackageDependency"},{"name":"RegistryPackageFile"},{"name":"RegistryPackageTag"},{"name":"RegistryPackageVersion"},{"name":"Release"},{"name":"ReleaseAsset"},{"name":"RemovedFromProjectEvent"},{"name":"RenamedTitleEvent"},{"name":"ReopenedEvent"},{"name":"Repository"},{"name":"RepositoryInvitation"},{"name":"RepositoryTopic"},{"name":"ReviewDismissalAllowance"},{"name":"ReviewDismissedEvent"},{"name":"ReviewRequest"},{"name":"ReviewRequestRemovedEvent"},{"name":"ReviewRequestedEvent"},{"name":"SavedReply"},{"name":"SecurityAdvisory"},{"name":"SponsorsListing"},{"name":"Sponsorship"},{"name":"Status"},{"name":"StatusContext"},{"name":"SubscribedEvent"},{"name":"Tag"},{"name":"Team"},{"name":"Topic"},{"name":"TransferredEvent"},{"name":"Tree"},{"name":"UnassignedEvent"},{"name":"UnlabeledEvent"},{"name":"UnlockedEvent"},{"name":"UnpinnedEvent"},{"name":"UnsubscribedEvent"},{"name":"User"},{"name":"UserBlockedEvent"},{"name":"UserContentEdit"},{"name":"UserStatus"}]},{"kind":"INTERFACE","name":"UniformResourceLocatable","possibleTypes":[{"name":"Bot"},{"name":"ClosedEvent"},{"name":"Commit"},{"name":"CrossReferencedEvent"},{"name":"Gist"},{"name":"Issue"},{"name":"Mannequin"},{"name":"MergedEvent"},{"name":"Milestone"},{"name":"Organization"},{"name":"PullRequest"},{"name":"PullRequestCommit"},{"name":"ReadyForReviewEvent"},{"name":"Release"},{"name":"Repository"},{"name":"RepositoryTopic"},{"name":"ReviewDismissedEvent"},{"name":"User"}]},{"kind":"INTERFACE","name":"Actor","possibleTypes":[{"name":"Bot"},{"name":"Mannequin"},{"name":"Organization"},{"name":"User"}]},{"kind":"INTERFACE","name":"RegistryPackageOwner","possibleTypes":[{"name":"Organization"},{"name":"Repository"},{"name":"User"}]},{"kind":"INTERFACE","name":"ProjectOwner","possibleTypes":[{"name":"Organization"},{"name":"Repository"},{"name":"User"}]},{"kind":"INTERFACE","name":"Closable","possibleTypes":[{"name":"Issue"},{"name":"Milestone"},{"name":"Project"},{"name":"PullRequest"}]},{"kind":"INTERFACE","name":"Updatable","possibleTypes":[{"name":"CommitComment"},{"name":"GistComment"},{"name":"Issue"},{"name":"IssueComment"},{"name":"Project"},{"name":"PullRequest"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"}]},{"kind":"UNION","name":"ProjectCardItem","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"INTERFACE","name":"Assignable","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"INTERFACE","name":"Comment","possibleTypes":[{"name":"CommitComment"},{"name":"GistComment"},{"name":"Issue"},{"name":"IssueComment"},{"name":"PullRequest"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"}]},{"kind":"INTERFACE","name":"UpdatableComment","possibleTypes":[{"name":"CommitComment"},{"name":"GistComment"},{"name":"Issue"},{"name":"IssueComment"},{"name":"PullRequest"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"}]},{"kind":"INTERFACE","name":"Labelable","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"INTERFACE","name":"Lockable","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"INTERFACE","name":"RegistryPackageSearch","possibleTypes":[{"name":"Organization"},{"name":"User"}]},{"kind":"INTERFACE","name":"RepositoryOwner","possibleTypes":[{"name":"Organization"},{"name":"User"}]},{"kind":"INTERFACE","name":"MemberStatusable","possibleTypes":[{"name":"Organization"},{"name":"Team"}]},{"kind":"INTERFACE","name":"ProfileOwner","possibleTypes":[{"name":"Organization"},{"name":"User"}]},{"kind":"UNION","name":"PinnableItem","possibleTypes":[{"name":"Gist"},{"name":"Repository"}]},{"kind":"INTERFACE","name":"Starrable","possibleTypes":[{"name":"Gist"},{"name":"Repository"},{"name":"Topic"}]},{"kind":"INTERFACE","name":"RepositoryInfo","possibleTypes":[{"name":"Repository"}]},{"kind":"INTERFACE","name":"GitObject","possibleTypes":[{"name":"Blob"},{"name":"Commit"},{"name":"Tag"},{"name":"Tree"}]},{"kind":"INTERFACE","name":"RepositoryNode","possibleTypes":[{"name":"CommitComment"},{"name":"CommitCommentThread"},{"name":"Issue"},{"name":"IssueComment"},{"name":"PullRequest"},{"name":"PullRequestCommitCommentThread"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"}]},{"kind":"INTERFACE","name":"Subscribable","possibleTypes":[{"name":"Commit"},{"name":"Issue"},{"name":"PullRequest"},{"name":"Repository"},{"name":"Team"}]},{"kind":"INTERFACE","name":"Deletable","possibleTypes":[{"name":"CommitComment"},{"name":"GistComment"},{"name":"IssueComment"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"}]},{"kind":"INTERFACE","name":"Reactable","possibleTypes":[{"name":"CommitComment"},{"name":"Issue"},{"name":"IssueComment"},{"name":"PullRequest"},{"name":"PullRequestReview"},{"name":"PullRequestReviewComment"}]},{"kind":"INTERFACE","name":"GitSignature","possibleTypes":[{"name":"GpgSignature"},{"name":"SmimeSignature"},{"name":"UnknownSignature"}]},{"kind":"UNION","name":"RequestedReviewer","possibleTypes":[{"name":"User"},{"name":"Team"},{"name":"Mannequin"}]},{"kind":"UNION","name":"PullRequestTimelineItem","possibleTypes":[{"name":"Commit"},{"name":"CommitCommentThread"},{"name":"PullRequestReview"},{"name":"PullRequestReviewThread"},{"name":"PullRequestReviewComment"},{"name":"IssueComment"},{"name":"ClosedEvent"},{"name":"ReopenedEvent"},{"name":"SubscribedEvent"},{"name":"UnsubscribedEvent"},{"name":"MergedEvent"},{"name":"ReferencedEvent"},{"name":"CrossReferencedEvent"},{"name":"AssignedEvent"},{"name":"UnassignedEvent"},{"name":"LabeledEvent"},{"name":"UnlabeledEvent"},{"name":"MilestonedEvent"},{"name":"DemilestonedEvent"},{"name":"RenamedTitleEvent"},{"name":"LockedEvent"},{"name":"UnlockedEvent"},{"name":"DeployedEvent"},{"name":"DeploymentEnvironmentChangedEvent"},{"name":"HeadRefDeletedEvent"},{"name":"HeadRefRestoredEvent"},{"name":"HeadRefForcePushedEvent"},{"name":"BaseRefForcePushedEvent"},{"name":"ReviewRequestedEvent"},{"name":"ReviewRequestRemovedEvent"},{"name":"ReviewDismissedEvent"},{"name":"UserBlockedEvent"}]},{"kind":"UNION","name":"Closer","possibleTypes":[{"name":"Commit"},{"name":"PullRequest"}]},{"kind":"UNION","name":"ReferencedSubject","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"UNION","name":"Assignee","possibleTypes":[{"name":"Bot"},{"name":"Mannequin"},{"name":"Organization"},{"name":"User"}]},{"kind":"UNION","name":"MilestoneItem","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"UNION","name":"RenamedTitleSubject","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"UNION","name":"PullRequestTimelineItems","possibleTypes":[{"name":"PullRequestCommit"},{"name":"PullRequestCommitCommentThread"},{"name":"PullRequestReview"},{"name":"PullRequestReviewThread"},{"name":"PullRequestRevisionMarker"},{"name":"BaseRefChangedEvent"},{"name":"BaseRefForcePushedEvent"},{"name":"DeployedEvent"},{"name":"DeploymentEnvironmentChangedEvent"},{"name":"HeadRefDeletedEvent"},{"name":"HeadRefForcePushedEvent"},{"name":"HeadRefRestoredEvent"},{"name":"MergedEvent"},{"name":"ReviewDismissedEvent"},{"name":"ReviewRequestedEvent"},{"name":"ReviewRequestRemovedEvent"},{"name":"ReadyForReviewEvent"},{"name":"IssueComment"},{"name":"CrossReferencedEvent"},{"name":"AddedToProjectEvent"},{"name":"AssignedEvent"},{"name":"ClosedEvent"},{"name":"CommentDeletedEvent"},{"name":"ConvertedNoteToIssueEvent"},{"name":"DemilestonedEvent"},{"name":"LabeledEvent"},{"name":"LockedEvent"},{"name":"MentionedEvent"},{"name":"MilestonedEvent"},{"name":"MovedColumnsInProjectEvent"},{"name":"PinnedEvent"},{"name":"ReferencedEvent"},{"name":"RemovedFromProjectEvent"},{"name":"RenamedTitleEvent"},{"name":"ReopenedEvent"},{"name":"SubscribedEvent"},{"name":"TransferredEvent"},{"name":"UnassignedEvent"},{"name":"UnlabeledEvent"},{"name":"UnlockedEvent"},{"name":"UserBlockedEvent"},{"name":"UnpinnedEvent"},{"name":"UnsubscribedEvent"}]},{"kind":"UNION","name":"IssueOrPullRequest","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"}]},{"kind":"UNION","name":"IssueTimelineItem","possibleTypes":[{"name":"Commit"},{"name":"IssueComment"},{"name":"CrossReferencedEvent"},{"name":"ClosedEvent"},{"name":"ReopenedEvent"},{"name":"SubscribedEvent"},{"name":"UnsubscribedEvent"},{"name":"ReferencedEvent"},{"name":"AssignedEvent"},{"name":"UnassignedEvent"},{"name":"LabeledEvent"},{"name":"UnlabeledEvent"},{"name":"UserBlockedEvent"},{"name":"MilestonedEvent"},{"name":"DemilestonedEvent"},{"name":"RenamedTitleEvent"},{"name":"LockedEvent"},{"name":"UnlockedEvent"},{"name":"TransferredEvent"}]},{"kind":"UNION","name":"IssueTimelineItems","possibleTypes":[{"name":"IssueComment"},{"name":"CrossReferencedEvent"},{"name":"AddedToProjectEvent"},{"name":"AssignedEvent"},{"name":"ClosedEvent"},{"name":"CommentDeletedEvent"},{"name":"ConvertedNoteToIssueEvent"},{"name":"DemilestonedEvent"},{"name":"LabeledEvent"},{"name":"LockedEvent"},{"name":"MentionedEvent"},{"name":"MilestonedEvent"},{"name":"MovedColumnsInProjectEvent"},{"name":"PinnedEvent"},{"name":"ReferencedEvent"},{"name":"RemovedFromProjectEvent"},{"name":"RenamedTitleEvent"},{"name":"ReopenedEvent"},{"name":"SubscribedEvent"},{"name":"TransferredEvent"},{"name":"UnassignedEvent"},{"name":"UnlabeledEvent"},{"name":"UnlockedEvent"},{"name":"UserBlockedEvent"},{"name":"UnpinnedEvent"},{"name":"UnsubscribedEvent"}]},{"kind":"UNION","name":"ReviewDismissalAllowanceActor","possibleTypes":[{"name":"User"},{"name":"Team"}]},{"kind":"UNION","name":"PushAllowanceActor","possibleTypes":[{"name":"User"},{"name":"Team"}]},{"kind":"UNION","name":"PermissionGranter","possibleTypes":[{"name":"Organization"},{"name":"Repository"},{"name":"Team"}]},{"kind":"INTERFACE","name":"Sponsorable","possibleTypes":[{"name":"User"}]},{"kind":"INTERFACE","name":"Contribution","possibleTypes":[{"name":"CreatedCommitContribution"},{"name":"CreatedIssueContribution"},{"name":"CreatedPullRequestContribution"},{"name":"CreatedPullRequestReviewContribution"},{"name":"CreatedRepositoryContribution"},{"name":"JoinedGitHubContribution"},{"name":"RestrictedContribution"}]},{"kind":"UNION","name":"CreatedRepositoryOrRestrictedContribution","possibleTypes":[{"name":"CreatedRepositoryContribution"},{"name":"RestrictedContribution"}]},{"kind":"UNION","name":"CreatedIssueOrRestrictedContribution","possibleTypes":[{"name":"CreatedIssueContribution"},{"name":"RestrictedContribution"}]},{"kind":"UNION","name":"CreatedPullRequestOrRestrictedContribution","possibleTypes":[{"name":"CreatedPullRequestContribution"},{"name":"RestrictedContribution"}]},{"kind":"UNION","name":"SearchResultItem","possibleTypes":[{"name":"Issue"},{"name":"PullRequest"},{"name":"Repository"},{"name":"User"},{"name":"Organization"},{"name":"MarketplaceListing"},{"name":"App"}]},{"kind":"UNION","name":"CollectionItemContent","possibleTypes":[{"name":"Repository"},{"name":"Organization"},{"name":"User"}]}]}}
\ No newline at end of file
diff --git a/packages/netlify-cms-backend-github/src/fragments.js b/packages/netlify-cms-backend-github/src/fragments.js
new file mode 100644
index 00000000..042ad745
--- /dev/null
+++ b/packages/netlify-cms-backend-github/src/fragments.js
@@ -0,0 +1,76 @@
+import gql from 'graphql-tag';
+
+export const repository = gql`
+ fragment RepositoryParts on Repository {
+ id
+ }
+`;
+
+export const blobWithText = gql`
+ fragment BlobWithTextParts on Blob {
+ id
+ text
+ is_binary: isBinary
+ }
+`;
+
+export const object = gql`
+ fragment ObjectParts on GitObject {
+ id
+ sha: oid
+ }
+`;
+
+export const branch = gql`
+ fragment BranchParts on Ref {
+ commit: target {
+ ...ObjectParts
+ }
+ id
+ name
+ repository {
+ ...RepositoryParts
+ }
+ }
+ ${object}
+ ${repository}
+`;
+
+export const pullRequest = gql`
+ fragment PullRequestParts on PullRequest {
+ id
+ baseRefName
+ body
+ headRefName
+ headRefOid
+ number
+ state
+ title
+ merged_at: mergedAt
+ repository {
+ ...RepositoryParts
+ }
+ }
+ ${repository}
+`;
+
+export const treeEntry = gql`
+ fragment TreeEntryParts on TreeEntry {
+ path: name
+ sha: oid
+ type
+ mode
+ }
+`;
+
+export const fileEntry = gql`
+ fragment FileEntryParts on TreeEntry {
+ name
+ sha: oid
+ blob: object {
+ ... on Blob {
+ size: byteSize
+ }
+ }
+ }
+`;
diff --git a/packages/netlify-cms-backend-github/src/implementation.js b/packages/netlify-cms-backend-github/src/implementation.js
index 446dea28..0f7c1602 100644
--- a/packages/netlify-cms-backend-github/src/implementation.js
+++ b/packages/netlify-cms-backend-github/src/implementation.js
@@ -4,6 +4,7 @@ import semaphore from 'semaphore';
import { stripIndent } from 'common-tags';
import AuthenticationPage from './AuthenticationPage';
import API from './API';
+import GraphQLAPI from './GraphQLAPI';
const MAX_CONCURRENT_DOWNLOADS = 10;
@@ -59,12 +60,13 @@ export default class GitHub {
}
this.originRepo = config.getIn(['backend', 'repo'], '');
} else {
- this.repo = config.getIn(['backend', 'repo'], '');
+ this.repo = this.originRepo = config.getIn(['backend', 'repo'], '');
}
this.branch = config.getIn(['backend', 'branch'], 'master').trim();
this.api_root = config.getIn(['backend', 'api_root'], 'https://api.github.com');
this.token = '';
this.squash_merges = config.getIn(['backend', 'squash_merges']);
+ this.use_graphql = config.getIn(['backend', 'use_graphql']);
}
authComponent() {
@@ -155,11 +157,12 @@ export default class GitHub {
async authenticate(state) {
this.token = state.token;
- this.api = new API({
+ const apiCtor = this.use_graphql ? GraphQLAPI : API;
+ this.api = new apiCtor({
token: this.token,
branch: this.branch,
repo: this.repo,
- originRepo: this.useOpenAuthoring ? this.originRepo : undefined,
+ originRepo: this.originRepo,
api_root: this.api_root,
squash_merges: this.squash_merges,
useOpenAuthoring: this.useOpenAuthoring,
@@ -191,6 +194,9 @@ export default class GitHub {
logout() {
this.token = null;
+ if (typeof this.api.reset === 'function') {
+ return this.api.reset();
+ }
return;
}
@@ -243,7 +249,7 @@ export default class GitHub {
// Fetches a single entry.
getEntry(collection, slug, path) {
- const repoURL = `/repos/${this.useOpenAuthoring ? this.originRepo : this.repo}`;
+ const repoURL = `/repos/${this.originRepo}`;
return this.api.readFile(path, null, { repoURL }).then(data => ({
file: { path },
data,
diff --git a/packages/netlify-cms-backend-github/src/mutations.js b/packages/netlify-cms-backend-github/src/mutations.js
new file mode 100644
index 00000000..24bcdff9
--- /dev/null
+++ b/packages/netlify-cms-backend-github/src/mutations.js
@@ -0,0 +1,109 @@
+import gql from 'graphql-tag';
+import * as fragments from './fragments';
+
+// updateRef only works for branches at the moment
+export const updateBranch = gql`
+ mutation updateRef($input: UpdateRefInput!) {
+ updateRef(input: $input) {
+ branch: ref {
+ ...BranchParts
+ }
+ }
+ }
+ ${fragments.branch}
+`;
+
+// deleteRef only works for branches at the moment
+const deleteRefMutationPart = `
+deleteRef(input: $deleteRefInput) {
+ clientMutationId
+}
+`;
+export const deleteBranch = gql`
+ mutation deleteRef($deleteRefInput: DeleteRefInput!) {
+ ${deleteRefMutationPart}
+ }
+`;
+
+const closePullRequestMutationPart = `
+closePullRequest(input: $closePullRequestInput) {
+ clientMutationId
+ pullRequest {
+ ...PullRequestParts
+ }
+}
+`;
+
+export const closePullRequest = gql`
+ mutation closePullRequestAndDeleteBranch($closePullRequestInput: ClosePullRequestInput!) {
+ ${closePullRequestMutationPart}
+ }
+ ${fragments.pullRequest}
+`;
+
+export const closePullRequestAndDeleteBranch = gql`
+ mutation closePullRequestAndDeleteBranch(
+ $closePullRequestInput: ClosePullRequestInput!
+ $deleteRefInput: DeleteRefInput!
+ ) {
+ ${closePullRequestMutationPart}
+ ${deleteRefMutationPart}
+ }
+ ${fragments.pullRequest}
+`;
+
+const createPullRequestMutationPart = `
+ createPullRequest(input: $createPullRequestInput) {
+ clientMutationId
+ pullRequest {
+ ...PullRequestParts
+ }
+}
+ `;
+
+export const createPullRequest = gql`
+ mutation createPullRequest($createPullRequestInput: CreatePullRequestInput!) {
+ ${createPullRequestMutationPart}
+ }
+ ${fragments.pullRequest}
+`;
+
+export const createBranch = gql`
+ mutation createBranch($createRefInput: CreateRefInput!) {
+ createRef(input: $createRefInput) {
+ branch: ref {
+ ...BranchParts
+ }
+ }
+ }
+ ${fragments.branch}
+`;
+
+// createRef only works for branches at the moment
+export const createBranchAndPullRequest = gql`
+ mutation createBranchAndPullRequest(
+ $createRefInput: CreateRefInput!
+ $createPullRequestInput: CreatePullRequestInput!
+ ) {
+ createRef(input: $createRefInput) {
+ branch: ref {
+ ...BranchParts
+ }
+ }
+ ${createPullRequestMutationPart}
+ }
+ ${fragments.branch}
+ ${fragments.pullRequest}
+`;
+
+export const reopenPullRequest = gql`
+ mutation reopenPullRequest($reopenPullRequestInput: ReopenPullRequestInput!) {
+ reopenPullRequest(input: $reopenPullRequestInput) {
+ clientMutationId
+ pullRequest {
+ ...PullRequestParts
+ }
+ }
+ }
+ ${fragments.pullRequest}
+`;
diff --git a/packages/netlify-cms-backend-github/src/queries.js b/packages/netlify-cms-backend-github/src/queries.js
new file mode 100644
index 00000000..e560cbb7
--- /dev/null
+++ b/packages/netlify-cms-backend-github/src/queries.js
@@ -0,0 +1,274 @@
+import gql from 'graphql-tag';
+import * as fragments from './fragments';
+
+export const repoPermission = gql`
+ query repoPermission($owner: String!, $name: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ viewerPermission
+ }
+ }
+ ${fragments.repository}
+`;
+
+export const user = gql`
+ query {
+ viewer {
+ id
+ avatar_url: avatarUrl
+ name
+ login
+ }
+ }
+`;
+
+export const blob = gql`
+ query blob($owner: String!, $name: String!, $expression: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ object(expression: $expression) {
+ ... on Blob {
+ ...BlobWithTextParts
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.blobWithText}
+`;
+
+export const unpublishedBranchFile = gql`
+ query unpublishedBranchFile(
+ $headOwner: String!
+ $headRepoName: String!
+ $headExpression: String!
+ $baseOwner: String!
+ $baseRepoName: String!
+ $baseExpression: String!
+ ) {
+ head: repository(owner: $headOwner, name: $headRepoName) {
+ ...RepositoryParts
+ object(expression: $headExpression) {
+ ... on Blob {
+ ...BlobWithTextParts
+ }
+ }
+ }
+ base: repository(owner: $baseOwner, name: $baseRepoName) {
+ ...RepositoryParts
+ object(expression: $baseExpression) {
+ ... on Blob {
+ id
+ oid
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.blobWithText}
+`;
+
+export const statues = gql`
+ query statues($owner: String!, $name: String!, $sha: GitObjectID!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ object(oid: $sha) {
+ ...ObjectParts
+ ... on Commit {
+ status {
+ id
+ contexts {
+ id
+ context
+ state
+ target_url: targetUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.object}
+`;
+
+export const files = gql`
+ query files($owner: String!, $name: String!, $expression: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ object(expression: $expression) {
+ ...ObjectParts
+ ... on Tree {
+ entries {
+ ...FileEntryParts
+ }
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.object}
+ ${fragments.fileEntry}
+`;
+
+export const unpublishedPrBranches = gql`
+ query unpublishedPrBranches($owner: String!, $name: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ refs(refPrefix: "refs/heads/cms/", last: 50) {
+ nodes {
+ id
+ associatedPullRequests(last: 50, states: OPEN) {
+ nodes {
+ id
+ headRef {
+ id
+ name
+ prefix
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+`;
+
+const branchQueryPart = `
+branch: ref(qualifiedName: $qualifiedName) {
+ ...BranchParts
+}
+`;
+
+export const branch = gql`
+ query branch($owner: String!, $name: String!, $qualifiedName: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ ${branchQueryPart}
+ }
+ }
+ ${fragments.repository}
+ ${fragments.branch}
+`;
+
+export const repository = gql`
+ query repository($owner: String!, $name: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ }
+ }
+ ${fragments.repository}
+`;
+
+const pullRequestQueryPart = `
+pullRequest(number: $number) {
+ ...PullRequestParts
+}
+`;
+
+export const pullRequest = gql`
+ query pullRequest($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ id
+ ${pullRequestQueryPart}
+ }
+ }
+ ${fragments.pullRequest}
+`;
+
+export const pullRequestAndBranch = gql`
+ query pullRequestAndBranch($owner: String!, $name: String!, $origin_owner: String!, $origin_name: String!, $qualifiedName: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ ${branchQueryPart}
+ }
+ origin: repository(owner: $origin_owner, name: $origin_name) {
+ ...RepositoryParts
+ ${pullRequestQueryPart}
+ }
+ }
+ ${fragments.repository}
+ ${fragments.branch}
+ ${fragments.pullRequest}
+`;
+
+export const commitTree = gql`
+ query commitTree($owner: String!, $name: String!, $sha: GitObjectID!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ commit: object(oid: $sha) {
+ ...ObjectParts
+ ... on Commit {
+ tree {
+ ...ObjectParts
+ entries {
+ ...TreeEntryParts
+ }
+ }
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.object}
+ ${fragments.treeEntry}
+`;
+
+export const tree = gql`
+ query tree($owner: String!, $name: String!, $sha: GitObjectID!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ tree: object(oid: $sha) {
+ ...ObjectParts
+ ... on Tree {
+ entries {
+ ...TreeEntryParts
+ }
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.object}
+ ${fragments.treeEntry}
+`;
+
+export const pullRequestCommits = gql`
+ query pullRequestCommits($owner: String!, $name: String!, $number: Int!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ pullRequest(number: $number) {
+ id
+ commits(last: 100) {
+ nodes {
+ id
+ commit {
+ ...ObjectParts
+ parents(last: 100) {
+ nodes {
+ ...ObjectParts
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.object}
+`;
+
+export const fileSha = gql`
+ query fileSha($owner: String!, $name: String!, $expression: String!) {
+ repository(owner: $owner, name: $name) {
+ ...RepositoryParts
+ file: object(expression: $expression) {
+ ...ObjectParts
+ }
+ }
+ }
+ ${fragments.repository}
+ ${fragments.object}
+`;
diff --git a/packages/netlify-cms-lib-util/src/backendUtil.js b/packages/netlify-cms-lib-util/src/backendUtil.js
index a04ca2e6..9b011357 100644
--- a/packages/netlify-cms-lib-util/src/backendUtil.js
+++ b/packages/netlify-cms-lib-util/src/backendUtil.js
@@ -56,22 +56,24 @@ export const parseLinkHeader = flow([
fromPairs,
]);
-export const getPaginatedRequestIterator = (url, options = {}, linkHeaderRelName = 'next') => {
- let req = unsentRequest.fromFetchArguments(url, options);
- const next = async () => {
- if (!req) {
- return { done: true };
- }
+export const getAllResponses = async (url, options = {}, linkHeaderRelName = 'next') => {
+ const maxResponses = 30;
+ let responseCount = 1;
+ let req = unsentRequest.fromFetchArguments(url, options);
+
+ const pageResponses = [];
+
+ while (req && responseCount < maxResponses) {
const pageResponse = await unsentRequest.performRequest(req);
const linkHeader = pageResponse.headers.get('Link');
const nextURL = linkHeader && parseLinkHeader(linkHeader)[linkHeaderRelName];
- req = nextURL && unsentRequest.fromURL(nextURL);
- return { value: pageResponse };
- };
- return {
- [Symbol.asyncIterator]: () => ({
- next,
- }),
- };
+
+ const { headers = {} } = options;
+ req = nextURL && unsentRequest.fromFetchArguments(nextURL, { headers });
+ pageResponses.push(pageResponse);
+ responseCount++;
+ }
+
+ return pageResponses;
};
diff --git a/packages/netlify-cms-lib-util/src/index.js b/packages/netlify-cms-lib-util/src/index.js
index 3e5b03b8..41559eaa 100644
--- a/packages/netlify-cms-lib-util/src/index.js
+++ b/packages/netlify-cms-lib-util/src/index.js
@@ -19,7 +19,7 @@ import {
import unsentRequest from './unsentRequest';
import {
filterByPropExtension,
- getPaginatedRequestIterator,
+ getAllResponses,
parseLinkHeader,
parseResponse,
responseParser,
@@ -72,7 +72,7 @@ export {
unsentRequest,
filterByPropExtension,
parseLinkHeader,
- getPaginatedRequestIterator,
+ getAllResponses,
parseResponse,
responseParser,
loadScript,
diff --git a/yarn.lock b/yarn.lock
index a091a09e..9ac2022a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1857,6 +1857,19 @@
once "^1.4.0"
universal-user-agent "^2.1.0"
+"@octokit/request@^5.0.0":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.0.2.tgz#59a920451f24811c016ddc507adcc41aafb2dca5"
+ integrity sha512-z1BQr43g4kOL4ZrIVBMHwi68Yg9VbkRUyuAgqCp1rU3vbYa69+2gIld/+gHclw15bJWQnhqqyEb7h5a5EqgZ0A==
+ dependencies:
+ "@octokit/endpoint" "^5.1.0"
+ "@octokit/request-error" "^1.0.1"
+ deprecation "^2.0.0"
+ is-plain-object "^3.0.0"
+ node-fetch "^2.3.0"
+ once "^1.4.0"
+ universal-user-agent "^3.0.0"
+
"@octokit/rest@^16.16.0":
version "16.28.2"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.28.2.tgz#3fc3b8700046ab29ab1e2a4bdf49f89e94f7ba27"
@@ -1876,6 +1889,25 @@
universal-user-agent "^2.0.0"
url-template "^2.0.8"
+"@octokit/rest@^16.28.7":
+ version "16.28.7"
+ resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.28.7.tgz#a2c2db5b318da84144beba82d19c1a9dbdb1a1fa"
+ integrity sha512-cznFSLEhh22XD3XeqJw51OLSfyL2fcFKUO+v2Ep9MTAFfFLS1cK1Zwd1yEgQJmJoDnj4/vv3+fGGZweG+xsbIA==
+ dependencies:
+ "@octokit/request" "^5.0.0"
+ "@octokit/request-error" "^1.0.2"
+ atob-lite "^2.0.0"
+ before-after-hook "^2.0.0"
+ btoa-lite "^1.0.0"
+ deprecation "^2.0.0"
+ lodash.get "^4.4.2"
+ lodash.set "^4.3.2"
+ lodash.uniq "^4.5.0"
+ octokit-pagination-methods "^1.1.0"
+ once "^1.4.0"
+ universal-user-agent "^3.0.0"
+ url-template "^2.0.8"
+
"@sheerun/mutationobserver-shim@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b"
@@ -1971,6 +2003,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.10.tgz#51babf9c7deadd5343620055fc8aff7995c8b031"
integrity sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==
+"@types/node@>=6":
+ version "12.6.8"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c"
+ integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==
+
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
@@ -2026,6 +2063,11 @@
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-12.0.12.tgz#45dd1d0638e8c8f153e87d296907659296873916"
integrity sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==
+"@types/zen-observable@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
+ integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
+
"@webassemblyjs/ast@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
@@ -2172,6 +2214,21 @@
"@webassemblyjs/wast-parser" "1.8.5"
"@xtuc/long" "4.2.2"
+"@wry/context@^0.4.0":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8"
+ integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==
+ dependencies:
+ "@types/node" ">=6"
+ tslib "^1.9.3"
+
+"@wry/equality@^0.1.2":
+ version "0.1.9"
+ resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909"
+ integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==
+ dependencies:
+ tslib "^1.9.3"
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -2353,6 +2410,85 @@ anymatch@^2.0.0:
micromatch "^3.1.4"
normalize-path "^2.1.1"
+apollo-cache-inmemory@^1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.2.tgz#bbf2e4e1eacdf82b2d526f5c2f3b37e5acee3c5e"
+ integrity sha512-AyCl3PGFv5Qv1w4N9vlg63GBPHXgMCekZy5mhlS042ji0GW84uTySX+r3F61ZX3+KM1vA4m9hQyctrEGiv5XjQ==
+ dependencies:
+ apollo-cache "^1.3.2"
+ apollo-utilities "^1.3.2"
+ optimism "^0.9.0"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+
+apollo-cache@1.3.2, apollo-cache@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a"
+ integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg==
+ dependencies:
+ apollo-utilities "^1.3.2"
+ tslib "^1.9.3"
+
+apollo-client@^2.6.3:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.3.tgz#9bb2d42fb59f1572e51417f341c5f743798d22db"
+ integrity sha512-DS8pmF5CGiiJ658dG+mDn8pmCMMQIljKJSTeMNHnFuDLV0uAPZoeaAwVFiAmB408Ujqt92oIZ/8yJJAwSIhd4A==
+ dependencies:
+ "@types/zen-observable" "^0.8.0"
+ apollo-cache "1.3.2"
+ apollo-link "^1.0.0"
+ apollo-utilities "1.3.2"
+ symbol-observable "^1.0.2"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+ zen-observable "^0.8.0"
+
+apollo-link-context@^1.0.18:
+ version "1.0.18"
+ resolved "https://registry.yarnpkg.com/apollo-link-context/-/apollo-link-context-1.0.18.tgz#9e700e3314da8ded50057fee0a18af2bfcedbfc3"
+ integrity sha512-aG5cbUp1zqOHHQjAJXG7n/izeMQ6LApd/whEF5z6qZp5ATvcyfSNkCfy3KRJMMZZ3iNfVTs6jF+IUA8Zvf+zeg==
+ dependencies:
+ apollo-link "^1.2.12"
+ tslib "^1.9.3"
+
+apollo-link-http-common@^0.2.14:
+ version "0.2.14"
+ resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.14.tgz#d3a195c12e00f4e311c417f121181dcc31f7d0c8"
+ integrity sha512-v6mRU1oN6XuX8beVIRB6OpF4q1ULhSnmy7ScnHnuo1qV6GaFmDcbdvXqxIkAV1Q8SQCo2lsv4HeqJOWhFfApOg==
+ dependencies:
+ apollo-link "^1.2.12"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+
+apollo-link-http@^1.5.15:
+ version "1.5.15"
+ resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.15.tgz#106ab23bb8997bd55965d05855736d33119652cf"
+ integrity sha512-epZFhCKDjD7+oNTVK3P39pqWGn4LEhShAoA1Q9e2tDrBjItNfviiE33RmcLcCURDYyW5JA6SMgdODNI4Is8tvQ==
+ dependencies:
+ apollo-link "^1.2.12"
+ apollo-link-http-common "^0.2.14"
+ tslib "^1.9.3"
+
+apollo-link@^1.0.0, apollo-link@^1.2.12:
+ version "1.2.12"
+ resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.12.tgz#014b514fba95f1945c38ad4c216f31bcfee68429"
+ integrity sha512-fsgIAXPKThyMVEMWQsUN22AoQI+J/pVXcjRGAShtk97h7D8O+SPskFinCGEkxPeQpE83uKaqafB2IyWdjN+J3Q==
+ dependencies:
+ apollo-utilities "^1.3.0"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+ zen-observable-ts "^0.8.19"
+
+apollo-utilities@1.3.2, apollo-utilities@^1.3.0, apollo-utilities@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
+ integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
+ dependencies:
+ "@wry/equality" "^0.1.2"
+ fast-json-stable-stringify "^2.0.0"
+ ts-invariant "^0.4.0"
+ tslib "^1.9.3"
+
aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@@ -2905,6 +3041,11 @@ before-after-hook@^1.4.0:
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d"
integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==
+before-after-hook@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
+ integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
+
big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@@ -4102,10 +4243,10 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
-cypress@^3.1.5:
- version "3.3.1"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.3.1.tgz#8a127b1d9fa74bff21f111705abfef58d595fdef"
- integrity sha512-JIo47ZD9P3jAw7oaK7YKUoODzszJbNw41JmBrlMMiupHOlhmXvZz75htuo7mfRFPC9/1MDQktO4lX/V2+a6lGQ==
+cypress@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30"
+ integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg==
dependencies:
"@cypress/listr-verbose-renderer" "0.4.1"
"@cypress/xvfb" "1.2.4"
@@ -4120,20 +4261,19 @@ cypress@^3.1.5:
execa "0.10.0"
executable "4.1.1"
extract-zip "1.6.7"
- fs-extra "4.0.1"
+ fs-extra "5.0.0"
getos "3.1.1"
- glob "7.1.3"
is-ci "1.2.1"
is-installed-globally "0.1.0"
lazy-ass "1.6.0"
listr "0.12.0"
- lodash "4.17.11"
+ lodash "4.17.15"
log-symbols "2.2.0"
minimist "1.2.0"
moment "2.24.0"
ramda "0.24.1"
request "2.88.0"
- request-progress "0.4.0"
+ request-progress "3.0.0"
supports-color "5.5.0"
tmp "0.1.0"
url "0.11.0"
@@ -4574,6 +4714,11 @@ dot-prop@^4.1.1, dot-prop@^4.2.0:
dependencies:
is-obj "^1.0.0"
+dotenv@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.0.0.tgz#ed310c165b4e8a97bb745b0a9d99c31bda566440"
+ integrity sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==
+
duplexer@^0.1.1, duplexer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
@@ -4848,6 +4993,13 @@ eslint-plugin-babel@^5.3.0:
dependencies:
eslint-rule-composer "^0.3.0"
+eslint-plugin-cypress@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.6.0.tgz#c726dd1a312cd5234de94765ca79718a14edf0ef"
+ integrity sha512-/YmvhYFqjFLYw7llDl56U9KW+Z25TZJjDofT47aUnAFRRvOhj1y2nxJzZ1ZTnqBO1ja8gwRhtdT7LfTLg9ANJw==
+ dependencies:
+ globals "^11.12.0"
+
eslint-plugin-emotion@^10.0.7:
version "10.0.7"
resolved "https://registry.yarnpkg.com/eslint-plugin-emotion/-/eslint-plugin-emotion-10.0.7.tgz#e14d54a2f23c9701d66851a87a5eb35adb10c2a7"
@@ -5577,13 +5729,13 @@ from@~0:
resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=
-fs-extra@4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.1.tgz#7fc0c6c8957f983f57f306a24e5b9ddd8d0dd880"
- integrity sha1-f8DGyJV/mD9X8waiTlud3Y0N2IA=
+fs-extra@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd"
+ integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==
dependencies:
graceful-fs "^4.1.2"
- jsonfile "^3.0.0"
+ jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^7.0.0:
@@ -5595,6 +5747,15 @@ fs-extra@^7.0.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
+fs-extra@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
+ integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^4.0.0"
+ universalify "^0.1.0"
+
fs-minipass@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07"
@@ -5815,18 +5976,6 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
-glob@7.1.3:
- version "7.1.3"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
- integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
@@ -5890,7 +6039,7 @@ global@^4.3.0:
min-document "^2.19.0"
process "^0.11.10"
-globals@^11.1.0, globals@^11.7.0:
+globals@^11.1.0, globals@^11.12.0, globals@^11.7.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
@@ -5974,6 +6123,23 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+graceful-fs@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
+ integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
+
+graphql-tag@^2.10.1:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02"
+ integrity sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==
+
+graphql@^14.4.2:
+ version "14.4.2"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.4.2.tgz#553a7d546d524663eda49ed6df77577be3203ae3"
+ integrity sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==
+ dependencies:
+ iterall "^1.2.2"
+
gray-matter@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
@@ -7072,6 +7238,11 @@ istanbul-reports@^2.1.1:
dependencies:
handlebars "^4.1.2"
+iterall@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
+ integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
+
jest-changed-files@^24.8.0:
version "24.8.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
@@ -7601,13 +7772,6 @@ json5@^2.1.0:
dependencies:
minimist "^1.2.0"
-jsonfile@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
- integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=
- optionalDependencies:
- graceful-fs "^4.1.6"
-
jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
@@ -7952,7 +8116,12 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@4.17.11, lodash@^4.1.1, lodash@^4.11.2, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1:
+lodash@4.17.15:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+ integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
+
+lodash@^4.1.1, lodash@^4.11.2, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -8593,11 +8762,6 @@ node-addon-api@^1.6.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.6.3.tgz#3998d4593e2dca2ea82114670a4eb003386a9fe1"
integrity sha512-FXWH6mqjWgU8ewuahp4spec8LkroFZK2NicOv6bNwZC3kcwZUI8LeZdG80UzTSLLhK4T7MsgNwlYDVRlDdfTDg==
-node-eta@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.1.1.tgz#4066109b39371c761c72b7ebda9a9ea0a5de121f"
- integrity sha1-QGYQmzk3HHYccrfr2pqeoKXeEh8=
-
node-fetch-npm@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7"
@@ -9027,6 +9191,13 @@ opn@^5.5.0:
dependencies:
is-wsl "^1.1.0"
+optimism@^0.9.0:
+ version "0.9.6"
+ resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.9.6.tgz#5621195486b294c3bfc518d17ac47767234b029f"
+ integrity sha512-bWr/ZP32UgFCQAoSkz33XctHwpq2via2sBvGvO5JIlrU8gaiM0LvoKj3QMle9LWdSKlzKik8XGSerzsdfYLNxA==
+ dependencies:
+ "@wry/context" "^0.4.0"
+
optimist@0.6.x, optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -10754,13 +10925,12 @@ replace-ext@1.0.0:
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
-request-progress@0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-0.4.0.tgz#c1954e39086aa85269c5660bcee0142a6a70d7e7"
- integrity sha1-wZVOOQhqqFJpxWYLzuAUKmpw1+c=
+request-progress@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
+ integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
dependencies:
- node-eta "^0.1.1"
- throttleit "^0.0.2"
+ throttleit "^1.0.0"
request-promise-core@1.1.2:
version "1.1.2"
@@ -11200,6 +11370,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+simple-git@^1.124.0:
+ version "1.124.0"
+ resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.124.0.tgz#10a73cc1af303832b5c11720d4256e134fba35ca"
+ integrity sha512-ks9mBoO4ODQy/xGLC8Cc+YDvj/hho/IKgPhi6h5LI/sA+YUdHc3v0DEoHzM29VmulubpGCxMJUSFmyXNsjNMEA==
+ dependencies:
+ debug "^4.0.1"
+
simple-html-tokenizer@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz#05c2eec579ffffe145a030ac26cfea61b980fabe"
@@ -11963,7 +12140,7 @@ symbol-observable@1.0.1:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=
-symbol-observable@^1.2.0:
+symbol-observable@^1.0.2, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -12085,10 +12262,10 @@ throat@^4.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
-throttleit@^0.0.2:
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
- integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
+throttleit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
+ integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
through2@^2.0.0, through2@^2.0.2:
version "2.0.5"
@@ -12309,7 +12486,14 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
-tslib@^1.9.0:
+ts-invariant@^0.4.0:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
+ integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
+ dependencies:
+ tslib "^1.9.3"
+
+tslib@^1.9.0, tslib@^1.9.3:
version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
@@ -12566,6 +12750,13 @@ universal-user-agent@^2.0.0, universal-user-agent@^2.1.0:
dependencies:
os-name "^3.0.0"
+universal-user-agent@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-3.0.0.tgz#4cc88d68097bffd7ac42e3b7c903e7481424b4b9"
+ integrity sha512-T3siHThqoj5X0benA5H0qcDnrKGXzU8TKoX15x/tQHw1hQBvIEBHjxQ2klizYsqBOO/Q+WuxoQUihadeeqDnoA==
+ dependencies:
+ os-name "^3.0.0"
+
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -13222,3 +13413,16 @@ yauzl@2.4.1:
integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
dependencies:
fd-slicer "~1.0.1"
+
+zen-observable-ts@^0.8.19:
+ version "0.8.19"
+ resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"
+ integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==
+ dependencies:
+ tslib "^1.9.3"
+ zen-observable "^0.8.0"
+
+zen-observable@^0.8.0:
+ version "0.8.14"
+ resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7"
+ integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g==