@nu-art/github-backend 0.400.7
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.
- package/core/module-pack.d.ts +1 -0
- package/core/module-pack.js +22 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/modules/ModuleBE_Github.d.ts +32 -0
- package/modules/ModuleBE_Github.js +248 -0
- package/package.json +48 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ModulePack_Backend_Github: import("../modules/ModuleBE_Github.js").GithubModule_Class[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Permissions management system, define access level for each of
|
|
3
|
+
* your server apis, and restrict users by giving them access levels
|
|
4
|
+
*
|
|
5
|
+
* Copyright (C) 2020 Adam van der Kruk aka TacB0sS
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { ModuleBE_Github } from '../modules/ModuleBE_Github.js';
|
|
20
|
+
export const ModulePack_Backend_Github = [
|
|
21
|
+
ModuleBE_Github
|
|
22
|
+
];
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Module } from '@nu-art/ts-common';
|
|
2
|
+
import { RestEndpointMethodTypes } from '@octokit/rest';
|
|
3
|
+
import { ReposGetContentResponseData } from '@octokit/types';
|
|
4
|
+
type Config = {
|
|
5
|
+
appId: string;
|
|
6
|
+
privateKey: string;
|
|
7
|
+
userAgent: string;
|
|
8
|
+
gitOwner: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class GithubModule_Class extends Module<Config> {
|
|
11
|
+
private createClient;
|
|
12
|
+
private createClientWithJWT;
|
|
13
|
+
private getInstallationTokenFromClient;
|
|
14
|
+
getGithubInstallationToken(): Promise<string>;
|
|
15
|
+
getFile(repo: string, filePath: string, branch: string): Promise<string>;
|
|
16
|
+
private getLargeFile;
|
|
17
|
+
private getFileBySha;
|
|
18
|
+
getReleaseBranches(product: string): Promise<string[]>;
|
|
19
|
+
listBranches(repo: string): Promise<RestEndpointMethodTypes['repos']['listBranches']['response']['data']>;
|
|
20
|
+
getArchiveUrl(repo: string, branch: string): Promise<string>;
|
|
21
|
+
downloadArchive(url: string, branch: string): Promise<any>;
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param repo The name of the repo.
|
|
25
|
+
* @param branch The name of the branch.
|
|
26
|
+
*
|
|
27
|
+
* This API has an upper limit of 1,000 files for a directory.
|
|
28
|
+
*/
|
|
29
|
+
listDirectoryContents(repo: string, branch: string, _path: string): Promise<ReposGetContentResponseData | undefined>;
|
|
30
|
+
}
|
|
31
|
+
export declare const ModuleBE_Github: GithubModule_Class;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A backend boilerplate with example apis
|
|
3
|
+
*
|
|
4
|
+
* Copyright (C) 2018 Adam van der Kruk aka TacB0sS
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
import { BadImplementationException, currentTimeMillis, Exception, JwtTools, Minute, Module } from '@nu-art/ts-common';
|
|
19
|
+
import { Octokit } from '@octokit/rest';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { promisifyRequest } from '@nu-art/thunderstorm-backend';
|
|
22
|
+
export class GithubModule_Class extends Module {
|
|
23
|
+
createClient(token, prefix) {
|
|
24
|
+
const auth = `${prefix || 'token'} ${token}`;
|
|
25
|
+
const client = new Octokit({
|
|
26
|
+
userAgent: this.config.userAgent,
|
|
27
|
+
log: {
|
|
28
|
+
debug: this.logDebug.bind(this),
|
|
29
|
+
info: this.logInfo.bind(this),
|
|
30
|
+
warn: this.logWarning.bind(this),
|
|
31
|
+
error: this.logError.bind(this)
|
|
32
|
+
},
|
|
33
|
+
auth: auth
|
|
34
|
+
});
|
|
35
|
+
return client;
|
|
36
|
+
}
|
|
37
|
+
async createClientWithJWT() {
|
|
38
|
+
const ts = Math.floor(Date.now() / 1000.0);
|
|
39
|
+
const payload = {
|
|
40
|
+
// issued at time
|
|
41
|
+
iat: ts,
|
|
42
|
+
// JWT expiration time (10 minute maximum)
|
|
43
|
+
exp: ts + (10 * 60),
|
|
44
|
+
// GitHub App's identifier
|
|
45
|
+
iss: parseInt(this.config.appId)
|
|
46
|
+
};
|
|
47
|
+
const signedToken = await JwtTools.encode(payload, this.config.privateKey, { expiresIn: currentTimeMillis() + 10 * Minute });
|
|
48
|
+
return this.createClient(signedToken, 'Bearer');
|
|
49
|
+
}
|
|
50
|
+
async getInstallationTokenFromClient(client) {
|
|
51
|
+
const installations = await client.apps.listInstallations();
|
|
52
|
+
// Get installations that match GIT_OWNER.
|
|
53
|
+
const filteredInstallations = installations.data.filter((installation) => {
|
|
54
|
+
return installation.account.login === this.config.gitOwner;
|
|
55
|
+
});
|
|
56
|
+
// Handle/log cases where the number of matching installations of the Github App is different than one.
|
|
57
|
+
if (filteredInstallations.length > 1) {
|
|
58
|
+
let message = `Warning: more than one installations of the Github app with owner ${this.config.gitOwner} found.`;
|
|
59
|
+
message += ` Picking the first one...`;
|
|
60
|
+
this.logInfo(message);
|
|
61
|
+
}
|
|
62
|
+
else if (!filteredInstallations.length) {
|
|
63
|
+
this.logError('Could not create installation access token for the Github App.');
|
|
64
|
+
this.logError(`Error: No installation matches owner "${this.config.gitOwner}."`);
|
|
65
|
+
this.logError(`Installations were: ${JSON.stringify(installations, null, 2)}`);
|
|
66
|
+
throw new BadImplementationException(`No installations for owner ${this.config.gitOwner}`);
|
|
67
|
+
}
|
|
68
|
+
// Create an "Installation access token". Expires in one hour. Identifies actions
|
|
69
|
+
// as performed by the Github App.
|
|
70
|
+
const installationToken = await client.apps.createInstallationAccessToken({ installation_id: filteredInstallations[0].id });
|
|
71
|
+
if (!installationToken.data || !installationToken.data.token) {
|
|
72
|
+
this.logError(`Invalid structure of installation token object.`);
|
|
73
|
+
throw new Exception(`Invalid structure of installation token object.`);
|
|
74
|
+
}
|
|
75
|
+
return installationToken.data.token;
|
|
76
|
+
}
|
|
77
|
+
async getGithubInstallationToken() {
|
|
78
|
+
const client = await this.createClientWithJWT();
|
|
79
|
+
const token = await this.getInstallationTokenFromClient(client);
|
|
80
|
+
this.logInfo('Got github installation token successfully.');
|
|
81
|
+
return token;
|
|
82
|
+
}
|
|
83
|
+
async getFile(repo, filePath, branch) {
|
|
84
|
+
const token = await this.getGithubInstallationToken();
|
|
85
|
+
const client = this.createClient(token);
|
|
86
|
+
let contents;
|
|
87
|
+
try {
|
|
88
|
+
contents = await client.repos.getContent({
|
|
89
|
+
owner: this.config.gitOwner,
|
|
90
|
+
repo,
|
|
91
|
+
path: filePath,
|
|
92
|
+
ref: branch
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.logError(error);
|
|
97
|
+
if (error.status === 403 && error.errors && error.errors.length === 1 &&
|
|
98
|
+
error.errors[0].code === 'too_large') {
|
|
99
|
+
this.logWarning(`File ${filePath} is too large, will attempt to get as Blob.`);
|
|
100
|
+
return this.getLargeFile(client, repo, filePath, branch);
|
|
101
|
+
}
|
|
102
|
+
else if (error.status === 404) {
|
|
103
|
+
this.logError(`File ${filePath} was not found.`);
|
|
104
|
+
throw new Exception(`File ${filePath} was not found`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw new Exception('Failed to get file from Github');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Check that if contents.data is not an array.
|
|
111
|
+
if (Array.isArray(contents.data)) {
|
|
112
|
+
throw new BadImplementationException('Invalid response of method repos.getContent');
|
|
113
|
+
}
|
|
114
|
+
if (!contents || !contents.data || !contents.data.content) {
|
|
115
|
+
throw new Exception('Failed to get file contents from Github');
|
|
116
|
+
}
|
|
117
|
+
const buffer = Buffer.from(contents.data.content, 'base64');
|
|
118
|
+
const decodedContent = buffer.toString('utf8');
|
|
119
|
+
return decodedContent;
|
|
120
|
+
}
|
|
121
|
+
async getLargeFile(client, repo, filePath, branch) {
|
|
122
|
+
const fileSha = await this.getFileBySha(client, repo, filePath, branch);
|
|
123
|
+
const request = {
|
|
124
|
+
owner: this.config.gitOwner,
|
|
125
|
+
repo,
|
|
126
|
+
file_sha: fileSha
|
|
127
|
+
};
|
|
128
|
+
const response = await client.git.getBlob(request);
|
|
129
|
+
if (!response || !response.data || !response.data.content)
|
|
130
|
+
throw new Exception('Failed to get file contents from Github');
|
|
131
|
+
const buffer = Buffer.from(response.data.content, 'base64');
|
|
132
|
+
return buffer.toString('utf8');
|
|
133
|
+
}
|
|
134
|
+
async getFileBySha(client, repo, filePath, branch) {
|
|
135
|
+
const parentPath = path.dirname(filePath);
|
|
136
|
+
let parentDirectoryResponse;
|
|
137
|
+
try {
|
|
138
|
+
const request = {
|
|
139
|
+
owner: this.config.gitOwner,
|
|
140
|
+
repo,
|
|
141
|
+
path: parentPath,
|
|
142
|
+
ref: branch
|
|
143
|
+
};
|
|
144
|
+
parentDirectoryResponse = await client.repos.getContent(request);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new Exception(`Failed to fetch parent directory contents of file ${filePath}`, error);
|
|
148
|
+
}
|
|
149
|
+
if (!parentDirectoryResponse || !parentDirectoryResponse.data)
|
|
150
|
+
throw new Exception(`Failed to fetch parent directory contents of file ${filePath}`);
|
|
151
|
+
// Check that if parentDirectoryResponse.data is an array.
|
|
152
|
+
if (!Array.isArray(parentDirectoryResponse.data))
|
|
153
|
+
throw new BadImplementationException('File\'s parent directory is not an array');
|
|
154
|
+
let fileSha = '';
|
|
155
|
+
for (const responseEntry of parentDirectoryResponse.data) {
|
|
156
|
+
if (responseEntry.path === filePath) {
|
|
157
|
+
fileSha = responseEntry.sha;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!fileSha)
|
|
162
|
+
throw new Exception(`File ${filePath} was not found`);
|
|
163
|
+
return fileSha;
|
|
164
|
+
}
|
|
165
|
+
async getReleaseBranches(product) {
|
|
166
|
+
const branches = await this.listBranches(product);
|
|
167
|
+
if (!branches || !branches.length) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
// Response includes (besides branch name) extra information about the branch.
|
|
171
|
+
const releaseBranches = branches.map(branch => branch.name).filter(name => {
|
|
172
|
+
return name.startsWith(`release/`);
|
|
173
|
+
}).reverse();
|
|
174
|
+
// Return master with the release branches.
|
|
175
|
+
releaseBranches.unshift('master');
|
|
176
|
+
return releaseBranches;
|
|
177
|
+
}
|
|
178
|
+
async listBranches(repo) {
|
|
179
|
+
const token = await this.getGithubInstallationToken();
|
|
180
|
+
const client = this.createClient(token);
|
|
181
|
+
let branches;
|
|
182
|
+
try {
|
|
183
|
+
// Returns all the branches using the paginate method.
|
|
184
|
+
// Maximum allowed page size is 100.
|
|
185
|
+
branches = await client.paginate(
|
|
186
|
+
// Equivalent to 'GET /repos/:owner/:repo/branches?per_page=100'
|
|
187
|
+
client.repos.listBranches, {
|
|
188
|
+
owner: this.config.gitOwner,
|
|
189
|
+
repo,
|
|
190
|
+
per_page: 100
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.logError(error);
|
|
195
|
+
throw new Exception(`Failed to list ${repo} branches`);
|
|
196
|
+
}
|
|
197
|
+
// Response includes (besides branch name) extra information about the branch.
|
|
198
|
+
return branches;
|
|
199
|
+
}
|
|
200
|
+
async getArchiveUrl(repo, branch) {
|
|
201
|
+
const token = await this.getGithubInstallationToken();
|
|
202
|
+
const client = this.createClient(token);
|
|
203
|
+
const response = await client.repos.downloadArchive({
|
|
204
|
+
owner: this.config.gitOwner,
|
|
205
|
+
repo,
|
|
206
|
+
ref: branch,
|
|
207
|
+
archive_format: 'zipball'
|
|
208
|
+
});
|
|
209
|
+
if (!response || !response.url) {
|
|
210
|
+
throw new Exception(`Invalid response while getting archive url for branch ${branch} of repo ${repo}`);
|
|
211
|
+
}
|
|
212
|
+
this.logInfo(`Got archive url: ${response.url}.`);
|
|
213
|
+
return response.url;
|
|
214
|
+
}
|
|
215
|
+
async downloadArchive(url, branch) {
|
|
216
|
+
const response = await promisifyRequest({ uri: url, encoding: null });
|
|
217
|
+
if (!response || !response.body) {
|
|
218
|
+
throw new Exception(`Failed to download archive for branch ${branch} of product ${url}`);
|
|
219
|
+
}
|
|
220
|
+
this.logDebug(`Got archive in zip format.`);
|
|
221
|
+
// Returns a buffer.
|
|
222
|
+
return response.body;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
*
|
|
226
|
+
* @param repo The name of the repo.
|
|
227
|
+
* @param branch The name of the branch.
|
|
228
|
+
*
|
|
229
|
+
* This API has an upper limit of 1,000 files for a directory.
|
|
230
|
+
*/
|
|
231
|
+
async listDirectoryContents(repo, branch, _path) {
|
|
232
|
+
const token = await this.getGithubInstallationToken();
|
|
233
|
+
const client = this.createClient(token);
|
|
234
|
+
const response = await client.repos.getContent({
|
|
235
|
+
owner: this.config.gitOwner,
|
|
236
|
+
repo,
|
|
237
|
+
path: _path,
|
|
238
|
+
ref: branch,
|
|
239
|
+
});
|
|
240
|
+
if (!response || !response.data)
|
|
241
|
+
return;
|
|
242
|
+
if (!Array.isArray(response.data)) {
|
|
243
|
+
throw new Exception(`Invalid response from octokit's repos.getContent`);
|
|
244
|
+
}
|
|
245
|
+
return response.data;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export const ModuleBE_Github = new GithubModule_Class();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nu-art/github-backend",
|
|
3
|
+
"version": "0.400.7",
|
|
4
|
+
"description": "Github api Module Backend",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"TacB0sS",
|
|
7
|
+
"backend",
|
|
8
|
+
"boilerplate",
|
|
9
|
+
"github",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+ssh://git@github.com:nu-art-js/thunderstorm-boilerplate.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"directory": "dist",
|
|
18
|
+
"linkDirectory": true
|
|
19
|
+
},
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"author": "TacB0sS",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@nu-art/thunderstorm-backend": "0.400.7",
|
|
24
|
+
"@nu-art/thunderstorm-shared": "0.400.7",
|
|
25
|
+
"@nu-art/ts-common": "0.400.7",
|
|
26
|
+
"@octokit/plugin-paginate-rest": "2.2.1",
|
|
27
|
+
"@octokit/rest": "18.0.9",
|
|
28
|
+
"@octokit/types": "^5.5.0",
|
|
29
|
+
"jsonwebtoken": "^9.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/jsonwebtoken": "~8.5.0"
|
|
33
|
+
},
|
|
34
|
+
"unitConfig": {
|
|
35
|
+
"type": "typescript-lib"
|
|
36
|
+
},
|
|
37
|
+
"type": "module",
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./index.d.ts",
|
|
41
|
+
"import": "./index.js"
|
|
42
|
+
},
|
|
43
|
+
"./*": {
|
|
44
|
+
"types": "./*.d.ts",
|
|
45
|
+
"import": "./*.js"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|