UNPKG

16.9 kBJavaScriptView Raw
1/*
2 * Copyright (C) 2019 Forward Thinking, Inc. - All Rights Reserved
3 * Unauthorized copying of this file, via any medium is strictly prohibited
4 * Proprietary and confidential
5 */
6'use strict';
7
8process.env.DIGITALSPACES_DEBUG_RUNTIME_ID;
9process.env.DIGITALSPACES_CLI_BASE_URL;
10
11const _ = require('lodash');
12const __ = require('@digitalspaces/utils');
13const Debug = require('debug');
14const Config = require('./Config.js');
15
16class ApiInterface {
17
18 /**
19 *
20 * @param config {Config|object=}
21 * @returns {ApiInterface}
22 */
23 constructor(config) {
24
25 /** @type Config */
26 this.config = ( config instanceof Config ) ? config : new Config(( __.type(config) === 'object' ) ? config : undefined);
27
28 this.debug = Debug('ApiInterface');
29
30 return this;
31 }
32
33 /**
34 *
35 * @param blueprintRepo
36 * @param blueprintName
37 * @returns {Promise<boolean>}
38 */
39 async '/blueprints/exists'(blueprintRepo, blueprintName) {
40
41 if( __.type(blueprintRepo) !== 'string' || blueprintRepo === '' )
42 throw __.error('expect (blueprintRepo) to be a non-empty string.', {blueprintRepo});
43
44 if( __.type(blueprintName) !== 'string' || blueprintName === '' )
45 throw __.error('expect (blueprintName) to be a non-empty string.', {blueprintName});
46
47 return await this._request(`${blueprintRepo}/exists`, {args: [blueprintName], auth: true});
48
49 }
50
51 async '/blueprints/init'(blueprintRepo, blueprintName) {
52
53 if( __.type(blueprintRepo) !== 'string' || blueprintRepo === '' )
54 throw __.error('expect (blueprintRepo) to be a non-empty string.', {blueprintRepo});
55
56 if( __.type(blueprintName) !== 'string' || blueprintName === '' )
57 throw __.error('expect (blueprintName) to be a non-empty string.', {blueprintName});
58
59 return await this._request(`${blueprintRepo}/create`, {args: [blueprintName], auth: true});
60
61 }
62
63 /**
64 *
65 * @param blueprintRepo {string}
66 * @returns {Promise<array>}
67 */
68 async '/blueprints/list'(blueprintRepo) {
69
70 if( __.type(blueprintRepo) !== 'string' || blueprintRepo === '' )
71 throw __.error('expect (blueprintRepo) to be a non-empty string.', {blueprintRepo});
72
73 return await this._request(`${blueprintRepo}/list`, {auth: true});
74
75 }
76
77 async '/blueprints/repos/init'(options) {
78
79 const userId = await this.config.get('userId');
80
81 if( __.type(userId) !== 'string' || userId === '' )
82 throw __.error('expect (userId) to be a non-empty string.', {userId});
83
84 return this._request(`${userId}/repos/init`, {auth: true});
85
86 }
87
88 /**
89 *
90 * @returns {Promise<array>}
91 */
92 async '/blueprints/repos/list'() {
93
94 const userId = await this.config.get('userId');
95
96 if( __.type(userId) !== 'string' || userId === '' )
97 throw __.error('expect (userId) to be a non-empty string.', {userId});
98
99 return await this._request(`${userId}/repos/list`, {auth: true});
100 }
101
102 async '/blueprints/versions/exists'(blueprintRepo, blueprintName, blueprintVersion) {
103
104 if( __.type(blueprintRepo) !== 'string' || blueprintRepo === '' )
105 throw __.error('expect (blueprintRepo) to be a non-empty string.', {blueprintRepo});
106
107 if( __.type(blueprintName) !== 'string' || blueprintName === '' )
108 throw __.error('expect (blueprintName) to be a non-empty string.', {blueprintName});
109
110 if( __.type(blueprintVersion) !== 'string' || blueprintVersion === '' )
111 throw __.error('expect (blueprintVersion) to be a non-empty string.', {blueprintVersion});
112
113 return await this._request(`${blueprintRepo}/versions/exists`, {
114 args: [blueprintName, blueprintVersion],
115 auth: true
116 });
117 }
118
119 async '/blueprints/versions/list'(blueprintRepo, blueprintName) {
120
121 if( __.type(blueprintRepo) !== 'string' || blueprintRepo === '' )
122 throw __.error('expect (blueprintRepo) to be a non-empty string.', {blueprintRepo});
123
124 if( __.type(blueprintName) !== 'string' || blueprintName === '' )
125 throw __.error('expect (blueprintName) to be a non-empty string.', {blueprintName});
126
127 return await this._request(`${blueprintRepo}/versions/list`, {args: [blueprintName], auth: true});
128 }
129
130 async '/blueprints/versions/pull'(blueprintRepo, blueprintName, blueprintVersion) {
131
132 if( __.type(blueprintRepo) !== 'string' || blueprintRepo === '' )
133 throw __.error('expect (blueprintRepo) to be a non-empty string.', {blueprintRepo});
134
135 if( __.type(blueprintName) !== 'string' || blueprintName === '' )
136 throw __.error('expect (blueprintName) to be a non-empty string.', {blueprintName});
137
138 if( __.type(blueprintVersion) !== 'string' || blueprintVersion === '' )
139 throw __.error('expect (blueprintVersion) to be a non-empty string.', {blueprintVersion});
140
141 return await this._request(`${blueprintRepo}/versions/read`, {
142 args: [blueprintName, blueprintVersion],
143 auth: true
144 });
145 }
146
147 async '/blueprints/versions/push'(blueprintRepo, blueprintName, blueprintVersion, blueprintFileString) {
148
149 const BlueprintUtils = require('@digitalspaces/blueprint-utils');
150
151 if( __.type(blueprintRepo) !== 'string' || ! BlueprintUtils.isValidRepo(blueprintRepo) )
152 throw __.error('expect (blueprintRepo) to be a valid Blueprint Repository.', {blueprintRepo});
153
154 if( __.type(blueprintName) !== 'string' || ! BlueprintUtils.isValidName(blueprintName) )
155 throw __.error('expect (blueprintName) to be a valid Blueprint Name.', {blueprintName});
156
157 if( __.type(blueprintVersion) !== 'string' || ! BlueprintUtils.isValidVersion(blueprintVersion) )
158 throw __.error('expect (blueprintVersion) to be a valid Blueprint Version.', {blueprintVersion})
159
160 if( __.type(blueprintFileString) !== 'string' || blueprintFileString === '' )
161 throw __.error('expect (blueprintFileString) to be a non-empty string.', {blueprintFileString});
162
163 return await this._request(`${blueprintRepo}/versions/create`, {
164 args: [blueprintName, blueprintFileString, {blueprintVersion}],
165 auth: true
166 });
167 }
168
169 async '/blueprints/versions/push/dev'(blueprintRepo, blueprintName, blueprintFileString, {forceNewVersion} = {}) {
170
171 const BlueprintUtils = require('@digitalspaces/blueprint-utils');
172
173 if( __.type(blueprintRepo) !== 'string' || ! BlueprintUtils.isValidRepo(blueprintRepo) )
174 throw __.error('expect (blueprintRepo) to be a valid Blueprint Repository.', {blueprintRepo});
175
176 if( __.type(blueprintName) !== 'string' || ! BlueprintUtils.isValidName(blueprintName) )
177 throw __.error('expect (blueprintName) to be a valid Blueprint Name.', {blueprintName});
178
179 if( __.type(blueprintFileString) !== 'string' || blueprintFileString === '' )
180 throw __.error('expect (blueprintFileString) to be a non-empty string.', {blueprintFileString});
181
182 if( ! __.isUndef(forceNewVersion) && ! __.isBool(forceNewVersion) )
183 throw __.error('expect (forceNewVersion) to be a boolean if provided.', {'forceNewVersion': forceNewVersion});
184
185 return await this._request(`${blueprintRepo}/versions/create/dev`, {
186 args: [blueprintName, blueprintFileString, {forceNewVersion}],
187 auth: true
188 });
189 }
190
191 async '/objects/init'(spaceId, blueprintRepo, blueprintName, blueprintVersion, {initParams} = {}) {
192
193 if( __.type(spaceId) !== 'string' || ! __.regexps.uuid.test(spaceId) )
194 throw __.error('expect (spaceId) to be a uuid.', {spaceId});
195
196 if( initParams !== undefined ){
197
198 if(__.type(initParams) !== 'string' || initParams === '')
199 throw __.error('expect (initParams) to be a non-empty string<json> if provided.', {initParams});
200
201 try{
202 const json = JSON.parse(initParams);
203 }
204 catch(err){
205 throw __.error('expect (initParams) to be a valid JSON string if provided.', err)
206 }
207
208 }
209
210 return await this._request(`${spaceId}/$objects/init`, {args: [blueprintRepo, blueprintName, blueprintVersion, {initParams}], auth: true})
211 }
212
213 async '/objects/list'(spaceId) {
214
215 if( __.type(spaceId) !== 'string' || ! __.regexps.uuid.test(spaceId) )
216 throw __.error('expect (spaceId) to be a uuid.', {spaceId});
217
218 return await this._request(`${spaceId}/$objects/list`, {auth: true})
219
220 }
221
222 async '/objects/spaces/init'() {
223
224 const userId = await this.config.get('userId');
225
226 if( __.type(userId) !== 'string' || userId === '' )
227 throw __.error('expect (userId) to be a non-empty string.', {userId});
228
229 return await this._request(`${userId}/spaces/init`, {auth: true});
230 }
231
232 async '/objects/spaces/list'() {
233
234 const userId = await this.config.get('userId');
235
236 if( __.type(userId) !== 'string' || userId === '' )
237 throw __.error('expect (userId) to be a non-empty string.', {userId});
238
239 return await this._request(`${userId}/spaces/list`, {auth: true});
240 }
241
242 /**
243 *
244 * @returns {Promise<{debugRuntimeToken: string}>}
245 */
246 async '/objects/spaces/debug/authorize'(){
247
248 const userId = await this.config.get('userId');
249
250 if( __.type(userId) !== 'string' || userId === '' )
251 throw __.error('expect (userId) to be a non-empty string.', {userId});
252
253 const {debugRuntimeToken} = await this._request(`${userId}/spaces/debug/authorize`, {args: [userId], auth: true});
254
255 if( ! __.isStringNonEmpty(debugRuntimeToken) || ! __.regexps.authToken.test(debugRuntimeToken) )
256 throw __.error('expect (debugRuntimeToken) to be a non-empty string.', {debugRuntimeToken});
257
258 return {debugRuntimeToken};
259
260 }
261
262 async '/users/login'(email, password) {
263
264 if( ! __.regexps.email.test(email) )
265 throw __.error('expect (email) to be a valid email address.', {email});
266
267 if( ! __.regexps.password.test(password) )
268 throw __.error('expect (password) to be a valid complex password [0-9a-zA-Z ~!@#$%^&*()_+{}|:"<>?`\\-=[\\]\\\\;\',./]{8,}.');
269
270 return await this._request('g/users/login', {args: [email, password]});
271
272 }
273
274 async '/users/refreshToken'() {
275
276 const {userId, authToken} = await this.config.get();
277
278 if( __.type(userId) !== 'string' || userId === '' )
279 throw __.error('expect (userId) to be a non-empty string.', {userId});
280
281 if( __.type(authToken) !== 'string' || authToken === '' )
282 throw __.error('expect (authToken) to be a non-empty string.', {authToken});
283
284 return await this._request(`${userId}/tokens/refresh`, {args: [authToken]});
285 }
286
287 /**
288 *
289 * @param email
290 * @param password
291 * @param firstName
292 * @param lastName
293 * @returns {Promise<{authToken: string, userId: string}>}
294 */
295 async '/users/register'(email, password, {firstName, lastName} = {}) {
296
297 if( ! __.regexps.email.test(email) )
298 throw __.error('expect (email) to be a valid email address.', {email});
299
300 if( ! __.regexps.password.test(password) )
301 throw __.error('expect (password) to be a valid complex password [0-9a-zA-Z ~!@#$%^&*()_+{}|:"<>?`\\-=[\\]\\\\;\',./]{8,}.');
302
303 if( __.type(firstName) !== 'string' || firstName === '' )
304 throw __.error('expect (firstName) to be a non-empty string.', {firstName});
305
306 if( __.type(lastName) !== 'string' || lastName === '' )
307 throw __.error('expect (lastName) to be a non-empty string.', {lastName});
308
309 const response = await this._request('g/users/register', {args: [email, password, {firstName, lastName}]});
310
311 if( ! response )
312 throw __.error(['UnexpectedResponse'], {response});
313
314 if( ! _.isString(response.authToken) || ! __.regexps.authToken.test(response.authToken) )
315 throw __.error(['UnexpectedResponse'], {response});
316
317 if( ! _.isString(response.userId) || ! __.regexps.uuid.test(response.userId) )
318 throw __.error(['UnexpectedResponse'], {response});
319
320 return response;
321
322 }
323
324 async '/users/update'({firstName, lastName, email, password}, existingPassword) {
325
326 const details = {};
327
328 if( __.type(existingPassword) !== 'string' || existingPassword === '' )
329 throw __.error('expect (existingPassword) to be a non-empty string.', {existingPassword});
330
331 if( firstName !== undefined ) {
332 if( __.type(firstName) !== 'string' || firstName === '' )
333 throw __.error('expect (firstName) to be a non-empty string.', {firstName});
334 details.firstName = firstName;
335 }
336
337 if( lastName !== undefined ) {
338 if( __.type(lastName) !== 'string' || lastName === '' )
339 throw __.error('expect (lastName) to be a non-empty string.', {lastName});
340 details.lastName = lastName;
341 }
342
343 if( email !== undefined ) {
344 if( ! __.regexps.email.test(email) )
345 throw __.error('expect (email) to be a valid email address.', {email});
346 details.email = email;
347 }
348
349 if( password !== undefined ) {
350 if( ! __.regexps.password.test(password) )
351 throw __.error('expect (password) to be a valid password.', {password});
352 details.password = password;
353 }
354
355 if( _.isEmpty(details) )
356 throw __.error('expect (details) to not be empty.');
357
358 const userId = await this.config.get('userId');
359
360 if( __.type(userId) !== 'string' || userId === '' )
361 throw __.error('expect (userId) to be a non-empty string.', {userId});
362
363 return this._request(`${userId}/details/update`, {args: [details, existingPassword], auth: true});
364
365 }
366
367 /**
368 *
369 * @param url
370 * @param options
371 * @param options.method {string}
372 * @param options.args {array}
373 * @param options.auth {boolean|string} This means different things depending on whether we are using object/call or not. If we are using objects/call it will ask the user object to include auth information. If we are not it will include our own local token in the request header.
374 * @param options.returnRaw {boolean}
375 * @param options.refreshAuthToken {boolean}
376 * @returns {*}
377 * @private
378 */
379 async _request(url, options) {
380
381 const Superagent = require('superagent');
382
383 url = _.trimStart(_.trim(url), '/');
384
385 const baseUrl = await this.config.get('baseUrl');
386
387 if( ! baseUrl || ! _.isString(baseUrl) )
388 throw new Error();
389
390 let targetUrl = `${baseUrl}/${url}`;
391
392 let optMethod = 'get', optArgs, optAuth, optReturnRaw, optRefreshAuthToken;
393
394 if( options ) {
395
396 if( options.method !== undefined )
397 if( ! options.method || ! _.isString(options.method) )
398 throw new Error();
399 else
400 optMethod = _.toLower(options.method);
401
402 if( options.args !== undefined )
403 if( ! _.isArray(options.args) )
404 throw new Error();
405 else
406 optMethod = 'post', optArgs = options.args;
407
408 if( options.auth !== undefined )
409 if( ! _.isBoolean(options.auth) && ! _.isString(options.auth) )
410 throw new Error;
411 else
412 optAuth = options.auth;
413
414 if( options.returnRaw !== undefined )
415 if( ! _.isBoolean(options.returnRaw) )
416 throw new Error();
417 else
418 optReturnRaw = options.returnRaw;
419
420 if( options.refreshAuthToken !== undefined )
421 if( ! _.isBoolean(options.refreshAuthToken) )
422 throw new Error();
423 else
424 optRefreshAuthToken = options.refreshAuthToken;
425
426 }
427
428 let request, userId;
429
430 if( /^g\//.test(url) || (__.regexps.uuid.test(userId = await this.config.get('userId')) && (new RegExp(`^${userId}\/`)).test(url) ) ){
431
432 request = Superagent[optMethod](targetUrl);
433
434 if( optAuth ) {
435
436 const authToken = await this.config.get('authToken');
437
438 if( ! authToken || ! _.isString(authToken) )
439 throw __.error(['AuthError', 'AuthToken required. Trying logging in.'])
440
441 request.set('Authorization', `JWT ${authToken}`);
442 }
443
444 if( optArgs !== undefined )
445 request.send(optArgs);
446
447 }
448 else{ //If the url is going to anything other than global try to proxy the request through the user (making sure to only read the userId if the request is not to global
449
450 if( ! __.regexps.uuid.test(userId) )
451 throw __.error('expect (userId) to be a uuid.', {userId});
452
453 const authToken = await this.config.get('authToken');
454
455 if( ! authToken )
456 throw __.error(['AuthError', 'AuthToken required. Trying logging in.'])
457
458 const [, objectId, actionName] = url.match(/^([^/]+)\/(.+)/) || __.throw('expect (url) to be "${objectPath}/${objectAction}".', {url})
459
460 targetUrl = `${baseUrl}/${userId}/objects/call`;
461
462 if( optArgs !== undefined )
463 optArgs = [objectId, actionName, optArgs];
464 else
465 optMethod = 'post', optArgs = [objectId, actionName];
466
467 if( ! __.regexps.authToken.test(authToken) )
468 throw new Error();
469
470 if( optAuth === true )
471 optArgs[3] = {useAuthToken: true};
472
473 request = Superagent[optMethod](targetUrl);
474
475 request.set('Authorization', `JWT ${authToken}`);
476
477 request.send(optArgs);
478
479 }
480
481 //this.debug('_request():', {request});
482
483 let response;
484
485 try {
486
487 response = await request;
488
489 }
490 catch( err ) {
491
492 this.debug('_request(): response error', {response: err.response});
493
494 if( optReturnRaw )
495 return err.response;
496
497 if( err.response ){
498
499 if( err.response.status === 555 )
500 throw __.error(__.errorFromJSON(err.response.body));
501
502 throw __.error(`HTTP call returned status != 200. (status: ${err.response.status}) (name: ${err.response.body.error && err.response.body.error.name}) (message: ${err.response.body.error && err.response.body.error.message})`, {
503 status: err.response.status,
504 headers: err.response.headers,
505 body: err.response.body
506 });
507 }
508 else
509 throw __.error('HTTP call did not receive a response.', err);
510 }
511
512 if( optReturnRaw )
513 return response;
514
515 return response.body;
516 }
517
518}
519
520module.exports = ApiInterface;
521
522module.exports.ApiInterface = ApiInterface;