-
Notifications
You must be signed in to change notification settings - Fork 0
/
TravisClient.ts
370 lines (322 loc) · 10.2 KB
/
TravisClient.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
/*!
* Travis CI Client Library
*
* Copyright (c) 2018, imqueue.com <support@imqueue.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
import * as Url from 'url';
import {
TravisHttp,
JsonPayload,
TravisRequestArg,
TravisRequestDescription,
TravisRoutesDescription,
TravisAuthMessage,
TravisConfig,
FunctionObject
} from '.';
const DEFAULT_API_VERSION: string = '2.0.0';
const ARG: string = ':arg';
const ARG_CHAIN: string = `${ARG},${ARG}`;
const RX_ARG: RegExp = /(:[^\/]+|\*+)/g;
const RX_ARG_CHAIN: RegExp = new RegExp(`${ARG}\/${ARG}`, 'g');
const RX_ARG_CLEAN: RegExp = /[:?]+/g;
const RX_ARG_OPTIONAL: RegExp = /(\?|^\*+)$/;
const LEAF: any = Symbol('@leaf');
/**
* Checks if a given argument is plain object
*
* @param {any} obj
* @returns {boolean}
*/
export function isObject(obj: any): boolean {
return obj !== null &&
obj !== undefined &&
Object.prototype.toString.call(obj) === '[object Object]';
}
/**
* Change method name to be camelCase representation
*
* @param {string} name
* @returns {string}
*/
export function toCamelCase(name: string): string {
return name.split('_').map((part, i) =>
i ? part.charAt(0).toUpperCase() + part.substr(1) : part
).join('');
}
/**
* Builds a url for a given request route replacing all urls args
* with the collected values.
*
* @access private
* @param {TravisRequestDescription} request
* @param {TravisRequestArg[]} args
* @returns {string}
*/
function buildUrl(
request: TravisRequestDescription,
args: TravisRequestArg[]
): string {
let uri = request.sourceUri;
const expected = (uri.match(RX_ARG) || []);
if (expected.length > args.length) {
const missing = expected.slice(args.length)
.filter(arg => !RX_ARG_OPTIONAL.test(arg))
.map(name => name.replace(RX_ARG_CLEAN, ''));
throw new TypeError(
`Argument${missing.length > 1 ? 's': ''} "${missing.join(', ')
}" expected, but was not given!`
);
}
for (let i = 0, s = expected.length; i < s; i++) {
const type = typeof args[i];
if (!~['string', 'number'].indexOf(type)) {
throw new TypeError(
`Argument "${expected[i]}" expected to be of type string, ` +
`but ${type} given!`
);
}
uri = uri.replace(`${expected[i]}`, String(args[i]));
}
return uri;
}
/**
* Builds routes cache from given API
*
* @access private
* @param {object} api
* @returns {TravisRoutesDescription}
*/
function buildRoutes(api: any): TravisRoutesDescription {
// noinspection TypeScriptUnresolvedVariable
return api.map((section: any) => section.routes)
.reduce((curr: any[], next: any[]) => curr.concat(next), [])
.map((route: any) => {
const { uri, verb, scope } = route;
const matchUri = uri.replace(RX_ARG, ARG);
const ret: any = { uri: matchUri, verb, scope, sourceUri: uri };
ret[LEAF] = true;
return ret;
}).reduce((map: any, route: any) => {
const method = route.verb.toLowerCase();
map[route.uri] = map[route.uri] || {};
map[route.uri][method] = route;
return map;
}, {});
}
/**
* Builds execution tree from a given routes
*
* @access private
* @param routes
* @returns void
*/
function buildRoutesTree(routes: TravisRoutesDescription): void {
const tree: any = {};
Object.keys(routes).map((route): [string[], TravisRequestDescription] => [
route.replace(RX_ARG_CHAIN, ARG_CHAIN)
.split('/').filter(name => name),
routes[route]
]).forEach((route: [string[], TravisRequestDescription]) => {
const [ paths, exec ] = route;
let branch = tree;
paths.forEach(name => {
name = toCamelCase(name);
branch[name] = branch[name] || {};
branch = branch[name];
});
Object.assign(branch, exec);
});
walk.call(this, this, tree);
}
/**
* Sets given name non-enumerable attribute to a given object
*
* @param {T} obj
* @param {string} name
* @returns {T}
*/
function setName<T>(obj: T, name: string): T {
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: false,
value: name
});
return obj;
}
/**
* Walks execution tree and creates execution contexts
*
* @param {FunctionObject} context - context to match a given tree
* @param {any} tree - related to context subtree
* @param {TravisRequestArg[]} [userInput] - collected user inputs during
* context execution
* @returns {FunctionObject}
*/
function walk(
context: FunctionObject,
tree: any,
userInput: TravisRequestArg[] = []
): FunctionObject {
Object.keys(tree).forEach(method => {
if (context[method]) {
return ;
}
else if (tree[method][LEAF]) {
context[method] = setName<FunctionObject>(async (
data?: JsonPayload
) => {
const url = buildUrl(tree[method], userInput);
return await this.agent.request(tree[method].verb, url, data);
}, method);
return ;
}
else if (method.charAt(0) === ':') {
walk.call(this, context, tree[method], userInput);
return ;
}
context[method] = setName<FunctionObject>((
...args: TravisRequestArg[]
) => {
const key = new Array(args.length).fill(ARG).join(',');
const newContext = setName(() => {}, method);
if (context === this) {
userInput = [];
}
userInput.push.apply(userInput, args);
if (!key) {
return walk.call(this, newContext, tree[method], userInput);
}
if (!tree[method][key]) {
throw new Error('Invalid number of arguments given!');
}
return walk.call(this, newContext, tree[method][key], userInput);
}, method);
walk.call(this, context[method], tree[method], userInput);
});
return context;
}
/**
* Loads REST API from a given version file
*
* @param {string} [version]
* @returns {any}
*/
function loadApi(version: string = DEFAULT_API_VERSION) {
return require(`../api/v${version}/routes.json`);
}
/**
* Checks if given message is valid object
*
* @param {TravisAuthMessage} msg
*/
function validateMessage(msg: TravisAuthMessage) {
if (!isObject(msg)) {
throw new TypeError('Given message is not object');
}
}
/**
* Checks if access token is valid in a given message
*
* @access private
* @param {TravisAuthMessage} msg
* @returns {Promise<object>}
*/
async function authenticateAccessToken(msg: TravisAuthMessage) {
validateMessage(msg);
if (!msg.access_token) {
throw new TypeError('Invalid access_token');
}
await this.agent.request('GET', '/users', msg);
this.agent.setAccessToken(msg.access_token);
return msg;
}
/**
* Authenticates github token from a given message
*
* @access private
* @param {object} msg
* @returns {Promise<object>}
*/
async function authenticateGithubToken(msg: TravisAuthMessage) {
validateMessage(msg);
if (!msg.github_token) {
throw new TypeError('Invalid github_token');
}
return await authenticateAccessToken.call(this,
await (this.auth as TravisClient).github.post(msg)
);
}
/**
* Class TravisClient
* Implements TravisCI REST API calls for node.
*/
export class TravisClient implements FunctionObject {
[name: string]: FunctionObject | any;
public pro: boolean = false;
public enterprise?: string | boolean;
public travisApiUrl: string;
public agent: TravisHttp;
/**
* @constructor
* @param {TravisConfig} [config]
*/
constructor(config?: TravisConfig) {
let { pro, enterprise, version, headers }: TravisConfig = config || {};
// noinspection PointlessBooleanExpressionJS
this.pro = !!pro;
this.enterprise = false;
this.travisApiUrl = `https://api.travis-ci.${pro ? 'com' : 'org'}`;
if (enterprise) {
const url = Url.parse(String(enterprise));
if (!url.protocol && url.host) {
throw new TypeError(`Expected a valid URL, got ${enterprise}`);
}
this.travisApiUrl = `${url.protocol}//${url.host}/api`;
this.enterprise = true;
}
this.agent = new TravisHttp(this.travisApiUrl, headers);
buildRoutesTree.call(this, buildRoutes(loadApi(version)));
}
/**
* Performs authentication using any of known methods:
* - using travis access token
* - using github oauth token
*
* @access public
* @param {TravisAuthMessage} msg - one of access or github tokens mandatory
* @returns {Promise<object>}
*/
async authenticate(msg: TravisAuthMessage): Promise<any> {
if (!isObject(msg)) {
throw new TypeError(`Expected an object, but ${typeof msg} given!`);
}
if (msg.access_token) {
return await authenticateAccessToken.call(this, msg);
}
else if (msg.github_token) {
return await authenticateGithubToken.call(this, msg);
}
throw new TypeError('Unexpected arguments!');
}
/**
* Checks if current travis instance has been authenticated already
*
* @returns {Promise<boolean>}
*/
isAuthenticated(): boolean {
return !!this.agent.getAccessToken();
}
}