@pranaysahith/decap-cms-backend-gitlab 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ import _styled from "@emotion/styled/base";
2
+ function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
3
+ import React from 'react';
4
+ import PropTypes from 'prop-types';
5
+ import { NetlifyAuthenticator, ImplicitAuthenticator, PkceAuthenticator } from 'decap-cms-lib-auth';
6
+ import { AuthenticationPage, Icon } from 'decap-cms-ui-default';
7
+ import { jsx as ___EmotionJSX } from "@emotion/react";
8
+ const LoginButtonIcon = /*#__PURE__*/_styled(Icon, {
9
+ target: "e80yw6v0",
10
+ label: "LoginButtonIcon"
11
+ })(process.env.NODE_ENV === "production" ? {
12
+ name: "1gnqu05",
13
+ styles: "margin-right:18px"
14
+ } : {
15
+ name: "1gnqu05",
16
+ styles: "margin-right:18px/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9BdXRoZW50aWNhdGlvblBhZ2UuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBVW9DIiwiZmlsZSI6Ii4uLy4uL3NyYy9BdXRoZW50aWNhdGlvblBhZ2UuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnO1xuaW1wb3J0IFByb3BUeXBlcyBmcm9tICdwcm9wLXR5cGVzJztcbmltcG9ydCBzdHlsZWQgZnJvbSAnQGVtb3Rpb24vc3R5bGVkJztcbmltcG9ydCB7XG4gIE5ldGxpZnlBdXRoZW50aWNhdG9yLFxuICBJbXBsaWNpdEF1dGhlbnRpY2F0b3IsXG4gIFBrY2VBdXRoZW50aWNhdG9yLFxufSBmcm9tICdkZWNhcC1jbXMtbGliLWF1dGgnO1xuaW1wb3J0IHsgQXV0aGVudGljYXRpb25QYWdlLCBJY29uIH0gZnJvbSAnZGVjYXAtY21zLXVpLWRlZmF1bHQnO1xuXG5jb25zdCBMb2dpbkJ1dHRvbkljb24gPSBzdHlsZWQoSWNvbilgXG4gIG1hcmdpbi1yaWdodDogMThweDtcbmA7XG5cbmNvbnN0IGNsaWVudFNpZGVBdXRoZW50aWNhdG9ycyA9IHtcbiAgcGtjZTogKHtcbiAgICBiYXNlX3VybCxcbiAgICBhdXRoX2VuZHBvaW50LFxuICAgIGFwcF9pZCxcbiAgICBhdXRoX3Rva2VuX2VuZHBvaW50fSkgPT5cbiAgICBuZXcgUGtjZUF1dGhlbnRpY2F0b3Ioe1xuICAgICAgYmFzZV91cmwsXG4gICAgICBhdXRoX2VuZHBvaW50LFxuICAgICAgYXBwX2lkLFxuICAgICAgYXV0aF90b2tlbl9lbmRwb2ludCxcbiAgICAgIGF1dGhfdG9rZW5fZW5kcG9pbnRfY29udGVudF90eXBlOiAnYXBwbGljYXRpb24vanNvbjsgY2hhcnNldD11dGYtOCcsXG4gICAgfSksXG5cbiAgaW1wbGljaXQ6ICh7XG4gICAgYmFzZV91cmwsXG4gICAgYXV0aF9lbmRwb2ludCxcbiAgICBhcHBfaWQsXG4gICAgY2xlYXJIYXNoIH0pID0+XG4gICAgbmV3IEltcGxpY2l0QXV0aGVudGljYXRvcih7XG4gICAgICBiYXNlX3VybCxcbiAgICAgIGF1dGhfZW5kcG9pbnQsXG4gICAgICBhcHBfaWQsXG4gICAgICBjbGVhckhhc2gsXG4gICAgfSksXG59O1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBHaXRMYWJBdXRoZW50aWNhdGlvblBhZ2UgZXh0ZW5kcyBSZWFjdC5Db21wb25lbnQge1xuICBzdGF0aWMgcHJvcFR5cGVzID0ge1xuICAgIG9uTG9naW46IFByb3BUeXBlcy5mdW5jLmlzUmVxdWlyZWQsXG4gICAgaW5Qcm9ncmVzczogUHJvcFR5cGVzLmJvb2wsXG4gICAgYmFzZV91cmw6IFByb3BUeXBlcy5zdHJpbmcsXG4gICAgc2l0ZUlkOiBQcm9wVHlwZXMuc3RyaW5nLFxuICAgIGF1dGhFbmRwb2ludDogUHJvcFR5cGVzLnN0cmluZyxcbiAgICBjb25maWc6IFByb3BUeXBlcy5vYmplY3QuaXNSZXF1aXJlZCxcbiAgICBjbGVhckhhc2g6IFByb3BUeXBlcy5mdW5jLFxuICAgIHQ6IFByb3BUeXBlcy5mdW5jLmlzUmVxdWlyZWQsXG4gIH07XG5cbiAgc3RhdGUgPSB7fTtcblxuICBjb21wb25lbnREaWRNb3VudCgpIHtcbiAgICAvLyBNYW51YWxseSB2YWxpZGF0ZSBQcm9wVHlwZXMgLSBSZWFjdCAxOSBicmVha2luZyBjaGFuZ2VcbiAgICBQcm9wVHlwZXMuY2hlY2tQcm9wVHlwZXMoR2l0TGFiQXV0aGVudGljYXRpb25QYWdlLnByb3BUeXBlcywgdGhpcy5wcm9wcywgJ3Byb3AnLCAnR2l0TGFiQXV0aGVudGljYXRpb25QYWdlJyk7XG5cbiAgICBjb25zdCB7XG4gICAgICBhdXRoX3R5cGU6IGF1dGhUeXBlID0gJycsXG4gICAgICBiYXNlX3VybCA9ICdodHRwczovL2dpdGxhYi5jb20nLFxuICAgICAgYXV0aF9lbmRwb2ludCA9ICdvYXV0aC9hdXRob3JpemUnLFxuICAgICAgYXBwX2lkID0gJycsXG4gICAgfSA9IHRoaXMucHJvcHMuY29uZmlnLmJhY2tlbmQ7XG5cbiAgICBpZiAoY2xpZW50U2lkZUF1dGhlbnRpY2F0b3JzW2F1dGhUeXBlXSkge1xuICAgICAgdGhpcy5hdXRoID0gY2xpZW50U2lkZUF1dGhlbnRpY2F0b3JzW2F1dGhUeXBlXSh7XG4gICAgICAgIGJhc2VfdXJsLFxuICAgICAgICBhdXRoX2VuZHBvaW50LFxuICAgICAgICBhcHBfaWQsXG4gICAgICAgIGF1dGhfdG9rZW5fZW5kcG9pbnQ6ICdvYXV0aC90b2tlbicsXG4gICAgICAgIGNsZWFySGFzaDogdGhpcy5wcm9wcy5jbGVhckhhc2gsXG4gICAgICB9KTtcbiAgICAgIC8vIENvbXBsZXRlIGltcGxpY2l0IGF1dGhlbnRpY2F0aW9uIGlmIHdlIHdlcmUgcmVkaXJlY3RlZCBiYWNrIHRvIGZyb20gdGhlIHByb3ZpZGVyLlxuICAgICAgdGhpcy5hdXRoLmNvbXBsZXRlQXV0aCgoZXJyLCBkYXRhKSA9PiB7XG4gICAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgICB0aGlzLnNldFN0YXRlKHsgbG9naW5FcnJvcjogZXJyLnRvU3RyaW5nKCkgfSk7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIHRoaXMucHJvcHMub25Mb2dpbihkYXRhKTtcbiAgICAgIH0pO1xuICAgIH0gZWxzZSB7XG4gICAgICB0aGlzLmF1dGggPSBuZXcgTmV0bGlmeUF1dGhlbnRpY2F0b3Ioe1xuICAgICAgICBiYXNlX3VybDogdGhpcy5wcm9wcy5iYXNlX3VybCxcbiAgICAgICAgc2l0ZV9pZDpcbiAgICAgICAgICBkb2N1bWVudC5sb2NhdGlvbi5ob3N0LnNwbGl0KCc6JylbMF0gPT09ICdsb2NhbGhvc3QnXG4gICAgICAgICAgICA/ICdkZW1vLmRlY2FwY21zLm9yZydcbiAgICAgICAgICAgIDogdGhpcy5wcm9wcy5zaXRlSWQsXG4gICAgICAgIGF1dGhfZW5kcG9pbnQ6IHRoaXMucHJvcHMuYXV0aEVuZHBvaW50LFxuICAgICAgfSk7XG4gICAgfVxuICB9XG5cbiAgaGFuZGxlTG9naW4gPSBlID0+IHtcbiAgICBlLnByZXZlbnREZWZhdWx0KCk7XG4gICAgdGhpcy5hdXRoLmF1dGhlbnRpY2F0ZSh7IHByb3ZpZGVyOiAnZ2l0bGFiJywgc2NvcGU6ICdhcGknIH0sIChlcnIsIGRhdGEpID0+IHtcbiAgICAgIGlmIChlcnIpIHtcbiAgICAgICAgdGhpcy5zZXRTdGF0ZSh7IGxvZ2luRXJyb3I6IGVyci50b1N0cmluZygpIH0pO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgICB0aGlzLnByb3BzLm9uTG9naW4oZGF0YSk7XG4gICAgfSk7XG4gIH07XG5cbiAgcmVuZGVyKCkge1xuICAgIGNvbnN0IHsgaW5Qcm9ncmVzcywgY29uZmlnLCB0IH0gPSB0aGlzLnByb3BzO1xuICAgIHJldHVybiAoXG4gICAgICA8QXV0aGVudGljYXRpb25QYWdlXG4gICAgICAgIG9uTG9naW49e3RoaXMuaGFuZGxlTG9naW59XG4gICAgICAgIGxvZ2luRGlzYWJsZWQ9e2luUHJvZ3Jlc3N9XG4gICAgICAgIGxvZ2luRXJyb3JNZXNzYWdlPXt0aGlzLnN0YXRlLmxvZ2luRXJyb3J9XG4gICAgICAgIGxvZ29Vcmw9e2NvbmZpZy5sb2dvX3VybH0gLy8gRGVwcmVjYXRlZCwgcmVwbGFjZWQgYnkgYGxvZ28uc3JjYFxuICAgICAgICBsb2dvPXtjb25maWcubG9nb31cbiAgICAgICAgc2l0ZVVybD17Y29uZmlnLnNpdGVfdXJsfVxuICAgICAgICByZW5kZXJCdXR0b25Db250ZW50PXsoKSA9PiAoXG4gICAgICAgICAgPFJlYWN0LkZyYWdtZW50PlxuICAgICAgICAgICAgPExvZ2luQnV0dG9uSWNvbiB0eXBlPVwiZ2l0bGFiXCIgLz57JyAnfVxuICAgICAgICAgICAge2luUHJvZ3Jlc3MgPyB0KCdhdXRoLmxvZ2dpbmdJbicpIDogdCgnYXV0aC5sb2dpbldpdGhHaXRMYWInKX1cbiAgICAgICAgICA8L1JlYWN0LkZyYWdtZW50PlxuICAgICAgICApfVxuICAgICAgICB0PXt0fVxuICAgICAgLz5cbiAgICApO1xuICB9XG59XG4iXX0= */",
17
+ toString: _EMOTION_STRINGIFIED_CSS_ERROR__
18
+ });
19
+ const clientSideAuthenticators = {
20
+ pkce: ({
21
+ base_url,
22
+ auth_endpoint,
23
+ app_id,
24
+ auth_token_endpoint
25
+ }) => new PkceAuthenticator({
26
+ base_url,
27
+ auth_endpoint,
28
+ app_id,
29
+ auth_token_endpoint,
30
+ auth_token_endpoint_content_type: 'application/json; charset=utf-8'
31
+ }),
32
+ implicit: ({
33
+ base_url,
34
+ auth_endpoint,
35
+ app_id,
36
+ clearHash
37
+ }) => new ImplicitAuthenticator({
38
+ base_url,
39
+ auth_endpoint,
40
+ app_id,
41
+ clearHash
42
+ })
43
+ };
44
+ export default class GitLabAuthenticationPage extends React.Component {
45
+ static propTypes = {
46
+ onLogin: PropTypes.func.isRequired,
47
+ inProgress: PropTypes.bool,
48
+ base_url: PropTypes.string,
49
+ siteId: PropTypes.string,
50
+ authEndpoint: PropTypes.string,
51
+ config: PropTypes.object.isRequired,
52
+ clearHash: PropTypes.func,
53
+ t: PropTypes.func.isRequired
54
+ };
55
+ state = {};
56
+ componentDidMount() {
57
+ // Manually validate PropTypes - React 19 breaking change
58
+ PropTypes.checkPropTypes(GitLabAuthenticationPage.propTypes, this.props, 'prop', 'GitLabAuthenticationPage');
59
+ const {
60
+ auth_type: authType = '',
61
+ base_url = 'https://gitlab.com',
62
+ auth_endpoint = 'oauth/authorize',
63
+ app_id = ''
64
+ } = this.props.config.backend;
65
+ if (clientSideAuthenticators[authType]) {
66
+ this.auth = clientSideAuthenticators[authType]({
67
+ base_url,
68
+ auth_endpoint,
69
+ app_id,
70
+ auth_token_endpoint: 'oauth/token',
71
+ clearHash: this.props.clearHash
72
+ });
73
+ // Complete implicit authentication if we were redirected back to from the provider.
74
+ this.auth.completeAuth((err, data) => {
75
+ if (err) {
76
+ this.setState({
77
+ loginError: err.toString()
78
+ });
79
+ return;
80
+ }
81
+ this.props.onLogin(data);
82
+ });
83
+ } else {
84
+ this.auth = new NetlifyAuthenticator({
85
+ base_url: this.props.base_url,
86
+ site_id: document.location.host.split(':')[0] === 'localhost' ? 'demo.decapcms.org' : this.props.siteId,
87
+ auth_endpoint: this.props.authEndpoint
88
+ });
89
+ }
90
+ }
91
+ handleLogin = e => {
92
+ e.preventDefault();
93
+ this.auth.authenticate({
94
+ provider: 'gitlab',
95
+ scope: 'api'
96
+ }, (err, data) => {
97
+ if (err) {
98
+ this.setState({
99
+ loginError: err.toString()
100
+ });
101
+ return;
102
+ }
103
+ this.props.onLogin(data);
104
+ });
105
+ };
106
+ render() {
107
+ const {
108
+ inProgress,
109
+ config,
110
+ t
111
+ } = this.props;
112
+ return ___EmotionJSX(AuthenticationPage, {
113
+ onLogin: this.handleLogin,
114
+ loginDisabled: inProgress,
115
+ loginErrorMessage: this.state.loginError,
116
+ logoUrl: config.logo_url // Deprecated, replaced by `logo.src`
117
+ ,
118
+ logo: config.logo,
119
+ siteUrl: config.site_url,
120
+ renderButtonContent: () => ___EmotionJSX(React.Fragment, null, ___EmotionJSX(LoginButtonIcon, {
121
+ type: "gitlab"
122
+ }), ' ', inProgress ? t('auth.loggingIn') : t('auth.loginWithGitLab')),
123
+ t: t
124
+ });
125
+ }
126
+ }
@@ -0,0 +1,358 @@
1
+ import trimStart from 'lodash/trimStart';
2
+ import semaphore from 'semaphore';
3
+ import trim from 'lodash/trim';
4
+ import { stripIndent } from 'common-tags';
5
+ import { CURSOR_COMPATIBILITY_SYMBOL, basename, entriesByFolder, entriesByFiles, getMediaDisplayURL, getMediaAsBlob, unpublishedEntries, getPreviewStatus, asyncLock, runWithLock, getBlobSHA, blobToFileObj, contentKeyFromBranch, generateContentKey, localForage, allEntriesByFolder, filterByExtension, branchFromContentKey, getDefaultBranchName } from 'decap-cms-lib-util';
6
+ import AuthenticationPage from './AuthenticationPage';
7
+ import API, { API_NAME } from './API';
8
+ const MAX_CONCURRENT_DOWNLOADS = 10;
9
+ export default class GitLab {
10
+ constructor(config, options = {}) {
11
+ this.options = {
12
+ proxied: false,
13
+ API: null,
14
+ initialWorkflowStatus: '',
15
+ ...options
16
+ };
17
+ if (!this.options.proxied && (config.backend.repo === null || config.backend.repo === undefined)) {
18
+ throw new Error('The GitLab backend needs a "repo" in the backend configuration.');
19
+ }
20
+ this.api = this.options.API || null;
21
+ this.repo = config.backend.repo || '';
22
+ this.branch = config.backend.branch || 'master';
23
+ this.isBranchConfigured = config.backend.branch ? true : false;
24
+ this.apiRoot = config.backend.api_root || 'https://gitlab.com/api/v4';
25
+ this.token = '';
26
+ this.squashMerges = config.backend.squash_merges || false;
27
+ this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
28
+ this.mediaFolder = config.media_folder;
29
+ this.previewContext = config.backend.preview_context || '';
30
+ this.useGraphQL = config.backend.use_graphql || false;
31
+ this.graphQLAPIRoot = config.backend.graphql_api_root || 'https://gitlab.com/api/graphql';
32
+ this.lock = asyncLock();
33
+ }
34
+ isGitBackend() {
35
+ return true;
36
+ }
37
+ async status() {
38
+ const auth = (await this.api?.user().then(user => !!user).catch(e => {
39
+ console.warn('Failed getting GitLab user', e);
40
+ return false;
41
+ })) || false;
42
+ return {
43
+ auth: {
44
+ status: auth
45
+ },
46
+ api: {
47
+ status: true,
48
+ statusPage: ''
49
+ }
50
+ };
51
+ }
52
+ authComponent() {
53
+ return AuthenticationPage;
54
+ }
55
+ restoreUser(user) {
56
+ return this.authenticate(user);
57
+ }
58
+ async authenticate(state) {
59
+ this.token = state.token;
60
+ this.api = new API({
61
+ token: this.token,
62
+ branch: this.branch,
63
+ repo: this.repo,
64
+ apiRoot: this.apiRoot,
65
+ squashMerges: this.squashMerges,
66
+ cmsLabelPrefix: this.cmsLabelPrefix,
67
+ initialWorkflowStatus: this.options.initialWorkflowStatus,
68
+ useGraphQL: this.useGraphQL,
69
+ graphQLAPIRoot: this.graphQLAPIRoot
70
+ });
71
+ const user = await this.api.user();
72
+ const isCollab = await this.api.hasWriteAccess().catch(error => {
73
+ error.message = stripIndent`
74
+ Repo "${this.repo}" not found.
75
+
76
+ Please ensure the repo information is spelled correctly.
77
+
78
+ If the repo is private, make sure you're logged into a GitLab account with access.
79
+ `;
80
+ throw error;
81
+ });
82
+
83
+ // Unauthorized user
84
+ if (!isCollab) {
85
+ throw new Error('Your GitLab user account does not have access to this repo.');
86
+ }
87
+ if (!this.isBranchConfigured) {
88
+ const defaultBranchName = await getDefaultBranchName({
89
+ backend: 'gitlab',
90
+ repo: this.repo,
91
+ token: this.token,
92
+ apiRoot: this.apiRoot
93
+ });
94
+ if (defaultBranchName) {
95
+ this.branch = defaultBranchName;
96
+ }
97
+ }
98
+ // Authorized user
99
+ return {
100
+ ...user,
101
+ login: user.username,
102
+ token: state.token
103
+ };
104
+ }
105
+ async logout() {
106
+ this.token = null;
107
+ return;
108
+ }
109
+ getToken() {
110
+ return Promise.resolve(this.token);
111
+ }
112
+ filterFile(folder, file, extension, depth) {
113
+ // gitlab paths include the root folder
114
+ const fileFolder = trim(file.path.split(folder)[1] || '/', '/');
115
+ return filterByExtension(file, extension) && fileFolder.split('/').length <= depth;
116
+ }
117
+ async entriesByFolder(folder, extension, depth) {
118
+ let cursor;
119
+ const listFiles = () => this.api.listFiles(folder, depth > 1).then(({
120
+ files,
121
+ cursor: c
122
+ }) => {
123
+ cursor = c.mergeMeta({
124
+ folder,
125
+ extension,
126
+ depth
127
+ });
128
+ return files.filter(file => this.filterFile(folder, file, extension, depth));
129
+ });
130
+ const files = await entriesByFolder(listFiles, this.api.readFile.bind(this.api), this.api.readFileMetadata.bind(this.api), API_NAME);
131
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
132
+ // @ts-ignore
133
+ files[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
134
+ return files;
135
+ }
136
+ async listAllFiles(folder, extension, depth) {
137
+ const files = await this.api.listAllFiles(folder, depth > 1);
138
+ const filtered = files.filter(file => this.filterFile(folder, file, extension, depth));
139
+ return filtered;
140
+ }
141
+ async allEntriesByFolder(folder, extension, depth) {
142
+ const files = await allEntriesByFolder({
143
+ listAllFiles: () => this.listAllFiles(folder, extension, depth),
144
+ readFile: this.api.readFile.bind(this.api),
145
+ readFileMetadata: this.api.readFileMetadata.bind(this.api),
146
+ apiName: API_NAME,
147
+ branch: this.branch,
148
+ localForage,
149
+ folder,
150
+ extension,
151
+ depth,
152
+ getDefaultBranch: () => this.api.getDefaultBranch().then(b => ({
153
+ name: b.name,
154
+ sha: b.commit.id
155
+ })),
156
+ isShaExistsInBranch: this.api.isShaExistsInBranch.bind(this.api),
157
+ getDifferences: (to, from) => this.api.getDifferences(to, from),
158
+ getFileId: path => this.api.getFileId(path, this.branch),
159
+ filterFile: file => this.filterFile(folder, file, extension, depth),
160
+ customFetch: this.useGraphQL ? files => this.api.readFilesGraphQL(files) : undefined
161
+ });
162
+ return files;
163
+ }
164
+ entriesByFiles(files) {
165
+ return entriesByFiles(files, this.api.readFile.bind(this.api), this.api.readFileMetadata.bind(this.api), API_NAME);
166
+ }
167
+
168
+ // Fetches a single entry.
169
+ getEntry(path) {
170
+ return this.api.readFile(path).then(data => ({
171
+ file: {
172
+ path,
173
+ id: null
174
+ },
175
+ data: data
176
+ }));
177
+ }
178
+ getMedia(mediaFolder = this.mediaFolder) {
179
+ return this.api.listAllFiles(mediaFolder).then(files => files.map(({
180
+ id,
181
+ name,
182
+ path
183
+ }) => {
184
+ return {
185
+ id,
186
+ name,
187
+ path,
188
+ displayURL: {
189
+ id,
190
+ name,
191
+ path
192
+ }
193
+ };
194
+ }));
195
+ }
196
+ getMediaDisplayURL(displayURL) {
197
+ this._mediaDisplayURLSem = this._mediaDisplayURLSem || semaphore(MAX_CONCURRENT_DOWNLOADS);
198
+ return getMediaDisplayURL(displayURL, this.api.readFile.bind(this.api), this._mediaDisplayURLSem);
199
+ }
200
+ async getMediaFile(path) {
201
+ const name = basename(path);
202
+ const blob = await getMediaAsBlob(path, null, this.api.readFile.bind(this.api));
203
+ const fileObj = blobToFileObj(name, blob);
204
+ const url = URL.createObjectURL(fileObj);
205
+ const id = await getBlobSHA(blob);
206
+ return {
207
+ id,
208
+ displayURL: url,
209
+ path,
210
+ name,
211
+ size: fileObj.size,
212
+ file: fileObj,
213
+ url
214
+ };
215
+ }
216
+ async persistEntry(entry, options) {
217
+ // persistEntry is a transactional operation
218
+ return runWithLock(this.lock, () => this.api.persistFiles(entry.dataFiles, entry.assets, options), 'Failed to acquire persist entry lock');
219
+ }
220
+ async persistMedia(mediaFile, options) {
221
+ const fileObj = mediaFile.fileObj;
222
+ const [id] = await Promise.all([getBlobSHA(fileObj), this.api.persistFiles([], [mediaFile], options)]);
223
+ const {
224
+ path
225
+ } = mediaFile;
226
+ const url = URL.createObjectURL(fileObj);
227
+ return {
228
+ displayURL: url,
229
+ path: trimStart(path, '/'),
230
+ name: fileObj.name,
231
+ size: fileObj.size,
232
+ file: fileObj,
233
+ url,
234
+ id
235
+ };
236
+ }
237
+ deleteFiles(paths, commitMessage) {
238
+ return this.api.deleteFiles(paths, commitMessage);
239
+ }
240
+ traverseCursor(cursor, action) {
241
+ return this.api.traverseCursor(cursor, action).then(async ({
242
+ entries,
243
+ cursor: newCursor
244
+ }) => {
245
+ const [folder, depth, extension] = [cursor.meta?.get('folder'), cursor.meta?.get('depth'), cursor.meta?.get('extension')];
246
+ if (folder && depth && extension) {
247
+ entries = entries.filter(f => this.filterFile(folder, f, extension, depth));
248
+ newCursor = newCursor.mergeMeta({
249
+ folder,
250
+ extension,
251
+ depth
252
+ });
253
+ }
254
+ const entriesWithData = await entriesByFiles(entries, this.api.readFile.bind(this.api), this.api.readFileMetadata.bind(this.api), API_NAME);
255
+ return {
256
+ entries: entriesWithData,
257
+ cursor: newCursor
258
+ };
259
+ });
260
+ }
261
+ loadMediaFile(branch, file) {
262
+ const readFile = (path, id, {
263
+ parseText
264
+ }) => this.api.readFile(path, id, {
265
+ branch,
266
+ parseText
267
+ });
268
+ return getMediaAsBlob(file.path, null, readFile).then(blob => {
269
+ const name = basename(file.path);
270
+ const fileObj = blobToFileObj(name, blob);
271
+ return {
272
+ id: file.path,
273
+ displayURL: URL.createObjectURL(fileObj),
274
+ path: file.path,
275
+ name,
276
+ size: fileObj.size,
277
+ file: fileObj
278
+ };
279
+ });
280
+ }
281
+ async loadEntryMediaFiles(branch, files) {
282
+ const mediaFiles = await Promise.all(files.map(file => this.loadMediaFile(branch, file)));
283
+ return mediaFiles;
284
+ }
285
+ async unpublishedEntries() {
286
+ const listEntriesKeys = () => this.api.listUnpublishedBranches().then(branches => branches.map(branch => contentKeyFromBranch(branch)));
287
+ const ids = await unpublishedEntries(listEntriesKeys);
288
+ return ids;
289
+ }
290
+ async unpublishedEntry({
291
+ id,
292
+ collection,
293
+ slug
294
+ }) {
295
+ if (id) {
296
+ const data = await this.api.retrieveUnpublishedEntryData(id);
297
+ return data;
298
+ } else if (collection && slug) {
299
+ const entryId = generateContentKey(collection, slug);
300
+ const data = await this.api.retrieveUnpublishedEntryData(entryId);
301
+ return data;
302
+ } else {
303
+ throw new Error('Missing unpublished entry id or collection and slug');
304
+ }
305
+ }
306
+ getBranch(collection, slug) {
307
+ const contentKey = generateContentKey(collection, slug);
308
+ const branch = branchFromContentKey(contentKey);
309
+ return branch;
310
+ }
311
+ async unpublishedEntryDataFile(collection, slug, path, id) {
312
+ const branch = this.getBranch(collection, slug);
313
+ const data = await this.api.readFile(path, id, {
314
+ branch
315
+ });
316
+ return data;
317
+ }
318
+ async unpublishedEntryMediaFile(collection, slug, path, id) {
319
+ const branch = this.getBranch(collection, slug);
320
+ const mediaFile = await this.loadMediaFile(branch, {
321
+ path,
322
+ id
323
+ });
324
+ return mediaFile;
325
+ }
326
+ async updateUnpublishedEntryStatus(collection, slug, newStatus) {
327
+ // updateUnpublishedEntryStatus is a transactional operation
328
+ return runWithLock(this.lock, () => this.api.updateUnpublishedEntryStatus(collection, slug, newStatus), 'Failed to acquire update entry status lock');
329
+ }
330
+ async deleteUnpublishedEntry(collection, slug) {
331
+ // deleteUnpublishedEntry is a transactional operation
332
+ return runWithLock(this.lock, () => this.api.deleteUnpublishedEntry(collection, slug), 'Failed to acquire delete entry lock');
333
+ }
334
+ async publishUnpublishedEntry(collection, slug) {
335
+ // publishUnpublishedEntry is a transactional operation
336
+ return runWithLock(this.lock, () => this.api.publishUnpublishedEntry(collection, slug), 'Failed to acquire publish entry lock');
337
+ }
338
+ async getDeployPreview(collection, slug) {
339
+ try {
340
+ const statuses = await this.api.getStatuses(collection, slug);
341
+ const deployStatus = getPreviewStatus(statuses, this.previewContext);
342
+ if (deployStatus) {
343
+ const {
344
+ target_url: url,
345
+ state
346
+ } = deployStatus;
347
+ return {
348
+ url,
349
+ status: state
350
+ };
351
+ } else {
352
+ return null;
353
+ }
354
+ } catch (e) {
355
+ return null;
356
+ }
357
+ }
358
+ }
@@ -0,0 +1,9 @@
1
+ import GitLabBackend from './implementation';
2
+ import API from './API';
3
+ import AuthenticationPage from './AuthenticationPage';
4
+ export const DecapCmsBackendGitlab = {
5
+ GitLabBackend,
6
+ API,
7
+ AuthenticationPage
8
+ };
9
+ export { GitLabBackend, API, AuthenticationPage };
@@ -0,0 +1,64 @@
1
+ import { gql } from 'graphql-tag';
2
+ import { oneLine } from 'common-tags';
3
+ export const files = gql`
4
+ query files($repo: ID!, $branch: String!, $path: String!, $recursive: Boolean!, $cursor: String) {
5
+ project(fullPath: $repo) {
6
+ repository {
7
+ tree(ref: $branch, path: $path, recursive: $recursive) {
8
+ blobs(after: $cursor) {
9
+ nodes {
10
+ type
11
+ id: sha
12
+ path
13
+ name
14
+ }
15
+ pageInfo {
16
+ endCursor
17
+ hasNextPage
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ `;
25
+ export const blobs = gql`
26
+ query blobs($repo: ID!, $branch: String!, $paths: [String!]!) {
27
+ project(fullPath: $repo) {
28
+ repository {
29
+ blobs(ref: $branch, paths: $paths) {
30
+ nodes {
31
+ id
32
+ data: rawBlob
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ `;
39
+ export function lastCommits(paths) {
40
+ const tree = paths.map((path, index) => oneLine`
41
+ tree${index}: tree(ref: $branch, path: "${path}") {
42
+ lastCommit {
43
+ authorName
44
+ authoredDate
45
+ author {
46
+ id
47
+ username
48
+ name
49
+ publicEmail
50
+ }
51
+ }
52
+ }
53
+ `).join('\n');
54
+ const query = gql`
55
+ query lastCommits($repo: ID!, $branch: String!) {
56
+ project(fullPath: $repo) {
57
+ repository {
58
+ ${tree}
59
+ }
60
+ }
61
+ }
62
+ `;
63
+ return query;
64
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@pranaysahith/decap-cms-backend-gitlab",
3
+ "description": "GitLab backend for Decap CMS",
4
+ "version": "3.4.0",
5
+ "repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-backend-gitlab",
6
+ "bugs": "https://github.com/decaporg/decap-cms/issues",
7
+ "license": "MIT",
8
+ "module": "dist/esm/index.js",
9
+ "main": "dist/decap-cms-backend-gitlab.js",
10
+ "keywords": [
11
+ "decap-cms",
12
+ "backend",
13
+ "gitlab"
14
+ ],
15
+ "sideEffects": false,
16
+ "scripts": {
17
+ "develop": "npm run build:esm -- --watch",
18
+ "build": "cross-env NODE_ENV=production webpack",
19
+ "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\""
20
+ },
21
+ "dependencies": {
22
+ "apollo-cache-inmemory": "^1.6.2",
23
+ "apollo-client": "^2.6.3",
24
+ "apollo-link-context": "^1.0.18",
25
+ "apollo-link-http": "^1.5.15",
26
+ "js-base64": "^3.0.0",
27
+ "path-browserify": "^1.0.1",
28
+ "semaphore": "^1.1.0"
29
+ },
30
+ "peerDependencies": {
31
+ "@emotion/react": "^11.11.1",
32
+ "@emotion/styled": "^11.11.0",
33
+ "@pranaysahith/decap-cms-lib-auth": "^3.0.0",
34
+ "@pranaysahith/decap-cms-lib-util": "^3.0.0",
35
+ "@pranaysahith/decap-cms-ui-default": "^3.0.0",
36
+ "immutable": "^3.7.6",
37
+ "lodash": "^4.17.11",
38
+ "prop-types": "^15.7.2",
39
+ "react": "^19.1.0"
40
+ },
41
+ "browser": {
42
+ "path": "path-browserify"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }