-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocloop-adapter.js
More file actions
494 lines (382 loc) · 16.3 KB
/
docloop-adapter.js
File metadata and controls
494 lines (382 loc) · 16.3 KB
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
'use strict'
var EventEmitter = require('events'),
Promise = require('bluebird'),
ObjectId = require('mongodb').ObjectID,
express = require('express'),
DocloopEndpoint = require('./docloop-endpoint.js').DocloopEndpoint,
DocloopError = require('./docloop-error-handling.js').DocloopError,
errorHandler = require('./docloop-error-handling.js').errorHandler,
catchAsyncErrors = require('./docloop-error-handling.js').catchAsyncErrors
/**
* Every adapter has its own reserved part of the express session object to store data in. SessionData refers to that part.
* Modifying SessionData will modify the session. Whenever SessionData is mentioned, it is assumed that there is an adapter it belongs to.
* If you come across the real express session object you can access the adapter's reserved session part by calling {@link DocloopAdapter#getSessionData}.
* The core and the base classes for adapters, links and endpoints use wrappers for some of the member methods to replace the original express session object with
* the adapters SessionData, in order to separate the data each adapter has access to. Wrapped methods are prefixed with an underscore _.
*
* @typedef {Object} SessionData
* @memberof DocloopAdapter
* @alias SessionData
*/
/**
* This Class and any Class that extends DocloopAdapter are supposed to be passed to DocloopCore.use().
*
* All methods that start with an underscore '_' expect the actual session to be passed as argument and will
* usually have a counterpart without the leading underscore that expects the adapter's session data as argument instead.
*
* The preferred way for an adapter to communicate with the core system or other adapters is through events. See {@link module:docloop.DocloopCore#relayEvent}.
*
* Tutorial: {@tutorial custom_adapters}
*
* @alias DocloopAdapter
*
* @memberof module:docloop
* @extends EventEmitter
*
* @param {DocloopCore} core An instance of the docloop core.
* @param {Object} config
* @param {String} config.id The id this adapter should be identified by (e.g. 'github').
* @param {String} [config.extraId] This maybe useful if a custom adapter has a fixed id, but is supposed to be instantiated twice with different configs.
* @param {String} [config.name='DocloopAdapter'] A pretty name for the Adapter, that can actually be used in a client.
* @param {String} config.type Either 'source' or 'target'.
* @param {EndpointClass} [config.endPointClass={@link DocloopEndpoint}] Endpoint class or any class extending Endpoint.
* @param {boolean} [config.extraEndpoints=false] True iff there are valid endpoints that are not returned by .getEndpoints()
* @param {Object} [config.endpointDefaultConfig = {}] Configuration object with default values for endpoints. These values will be used if no other values are provided when a new enpoint is created.
* @param {boolean} config.extraEndpoints true iff there are valid endpoints that are not returned by .getEndpoints()
*
* @property {DocloopCore} core
* @property {String} id
* @property {String} name
* @property {String} type
* @property {DocloopEndpoint} endPointClass
* @property {Object} endpointDefaultConfig
* @property {ExpressApp} app Express sub app of core.app. All routes will be relative to '/adapters/'+this.id.
* @propetty {Promise} ready Promise that resolves when core and adapter are fully set up.
* @property {Collection} endpoints Mongo-db collection for stored endpoints.
* @property {String} [help] Help text used by the client.
*/
class DocloopAdapter extends EventEmitter {
//Todo, put EventQueue config into adapter config
//TODO: Endpoint class into constructor
//TODO: DO not store decoration
//custom getDecoration function needed
//TODO: endpointDefaultConfig into Endpoint
constructor(core, config){
if(core === undefined) throw new ReferenceError("DocloopAdapter.constructor() missing core")
if(core.constructor.name != 'DocloopCore')
throw new TypeError("DocloopAdapter.constructor() core must be an instance of DocloopCore.")
if(!config) throw new ReferenceError("DocloopAdapter.constructor() missing config")
if(config.id === undefined) throw new ReferenceError("DocloopAdapter.constructor() missin config.id")
if(typeof config.id != 'string') throw new TypeError("DocloopAdapter.constructor() config.id must be a string, got: "+ (typeof config.id) )
if(config.extraId && typeof config.extraId != 'string')
throw new TypeError("DocloopAdapter.constructor() config.extraId must be a string")
if(config.name && typeof config.name != 'string')
throw new TypeError("DocloopAdapter.constructor() config.name must be a string")
if(config.type === undefined) throw new ReferenceError("DocloopAdapter.constructor() missing config.type")
if(['source', 'target'].indexOf(config.type.toLowerCase()) == -1)
throw new RangeError("DocloopAdapter.constructor()config.type must be either 'source' or 'target'; got "+config.type)
if(['source', 'target'].indexOf(config.type) == -1)
throw new RangeError("DocloopAdapter.constructor() type must be either 'source' or 'target', got: "+config.type)
if(config.extraEndpoints !== undefined && typeof config.extraEndpoints != 'boolean')
throw new TypeError("DocloopAdapter.constructor() config.extraEndpoints must be a boolean")
if(config.endpointDefaultConfig && typeof config.endpointDefaultConfig != 'object')
throw new TypeError("DocloopAdapter.constructor() config.endpointDefaultConfig must be an object")
if(config.endpointClass === undefined)
throw new ReferenceError("DocloopAdapter.constructor() missing config.endpointClass")
if(typeof config.endpointClass != 'function')
throw new TypeError("DocloopAdapter.constructor() config.endpointClass must be an instance of function; got: "+(typeof config.endpointClass))
super()
this.core = core
this.id = config.id + (config.extraId ? '-' + config.extraId :'')
this.name = config.name || 'DocloopAdapter'
this.type = config.type.toLowerCase()
this.endpointClass = config.endpointClass || DocloopEndpoint
this.extraEndpoints = !!config.extraEndpoints
this.endpointDefaultConfig = config.endpointDefaultConfig || {}
this.help = config.help
this.app = express()
this.app.get('/', catchAsyncErrors(this._handleGetRequest.bind(this) ))
this.app.get('/endpoints', catchAsyncErrors(this._handleGetEndpointsRequest.bind(this) ))
this.app.get('/guessEndpoint/:str', catchAsyncErrors(this._handleGetGuessEndpointRequest.bind(this) ))
this.app.post('/signoff', catchAsyncErrors(this._handlePostSignOff.bind(this) ))
this.setMaxListeners(50)
this.ready = this.core.ready
.then( () => {
this.endpoints = this.core.db.collection(this.id+'_endpoints')
this.app.use(errorHandler)
this.core.app.use('/adapters/'+this.id, this.app)
})
}
/**
* Extracts the data associated with this adapter in the provided session object. Modifying its values will modify the session.
*
* @param {Session} session Express session
*
* @return {SessionData} Adapter's session data
*/
_getSessionData(session){
if(session === undefined) throw new ReferenceError("DocloopAdapter._getSessionData() missing session")
if(session.constructor.name != "Session") throw new TypeError("DocloopAdapter._getSessionData() session must be instance of Session; got: "+session.constructor.name)
session.adapters = session.adapters || {}
session.adapters[this.id] = session.adapters[this.id] || {}
return session.adapters[this.id]
}
/**
* Clears the data associated with this adapter in the provided session object. Returns an empty object. Modifying its values will modify the session.
*
* @param {Session} session Express session
*
* @return {SessionData} Empty session data
*/
_clearSessionData(session){
if(session === undefined) throw new ReferenceError("DocloopAdapter._getSessionData() missing session")
if(session.constructor.name != "Session") throw new TypeError("DocloopAdapter._getSessionData() session must be instance of Session; got: "+session.constructor.name)
session.adapters = session.adapters || {}
session.adapters[this.id] = {}
return session.adapters[this.id]
}
//TODO: user logger?
/**
* Calls .getEndpoints() with {@link sessionData}.
*
* @async
*
* @param {Session} session Express session
*
* @return {DocloopEndpoint[]}
*/
async _getEndpoints(session){
return await this.getEndpoints(this._getSessionData(session))
}
/**
* Calls .getStoredEndpoint() with {@link sessionData}.
*
* @async
*
* @param {String|bson} id
* @param {Session} session Express session
*
* @return {ValidEndpoint}
*/
async _getStoredEndpoint(id, session){
return await this.getStoredEndpoint(id, this._getSessionData(session))
}
/**
* Calls .getStoredEndpoints with {@link sessionData}.
*
* @async
*
* @param {Session} session Express session
*
* @return {DocloopEndpoint[]}
*/
async _getStoredEndpoints(session){
return await this.getStoredEndpoints(this._getSessionData(session))
}
/**
* Calls .getAuthState() with {@link sessionData}.
*
* @async
*
* @param {Session} session Express session
*
* @return {AuthState}
*/
async _getAuthState(session){
return await this.getAuthState(this._getSessionData(session))
}
/**
* Data concerning an adapter meant for client use.
*
* @typedef {Object} AdapterData
* @memberof DocloopAdapter
* @alias AdapterData
*
* @property {String} id The adapter's id
* @property {String} name The adapter's name
* @property {String} type The adapter's type
* @property {boolean} extraEndpoints The adapter's .extraEndpoints value
* @property {Object} endpointDefaultConfig The adapter's .endpointDefaultConfig'value
* @property {AuthState} auth The adapters authorization state. (see ...)
*/
/**
* Collects adapter data for client use.
*
* @async
*
* @param {Session} session Express session
*
* @return {AdapterData}
*/
async _getData(session){
if(session === undefined) throw new ReferenceError("DocloopAdapter._getSessionData() missing session")
if(session.constructor.name != "Session") throw new TypeError("DocloopAdapter._getSessionData() session must be instance of Session; got: "+session.constructor.name)
return {
id: this.id,
name: this.name,
type: this.type,
extraEndpoints: this.extraEndpoints,
endpointDefaultConfig: this.endpointDefaultConfig,
auth: await this._getAuthState(session).catch(() => ({ user:null, link:null })),
help: this.help
}
}
/**
* Express request handler. Sends AdapterData to the client.
*
* @route {GET} /adapters/:adapter-id
*
* @param {Object} req Express request object
* @param {Object} res Express result object
*/
async _handleGetRequest(req, res){
var data = await this._getData(req.session)
res.status(200).send(data)
}
/**
* Express request handler. Sends privileged Enpoints to the client.
*
* @route {GET} /adapters/:adapter-id/endpoints
*
* @param {Object} req Express request object
* @param {Object} res Express result object
*/
async _handleGetEndpointsRequest(req, res){
var endpoints = await this._getEndpoints(req.session)
res.status(200).send(endpoints.map( endpoint => endpoint.export ))
}
/**
* Express request handler. Guesses Endpoint from request paramter and sends it to the client.
*
* @route {GET} /adapters/:adapter-id/guessEndpoint/:str
*
* @param {Object} req Express request object
* @param {Object} req.params Request parameters
* @param {String} req.params.str String to guess the endpoint from
* @param {Object} res Express result object
*/
async _handleGetGuessEndpointRequest(req,res){
var input = req && req.params && req.params.str
if(!input) throw new DocloopError("DocloopAdapter._handleGetGuessEndpointRequets() missing params.str", 400)
var endpoint = await this.endpointClass.guess(this, input, req.session)
res.status(200).send(endpoint.export)
}
/**
* Express request handler. Clears current session data for this adapter.
*
* @route {GET} /adapters/:adapter-id/signoff
*/
async _handlePostSignOff(req, res){
this._clearSessionData(req.session)
res.status(200).send(this.id+': session data cleared.')
}
/**
* Returns the endpoint with the provided id. Throws an error if it cannot be found or the session lacks privileges to access it.
*
* @async
*
* @param {String | bson} id Mongo-db id
*
* @throws {DocloopError} If the endpoint cannot be found.
* @throws {DocloopError} If the endpoint cannot be validated for the session.
*
* @return {Endpoint}
*/
async getStoredEndpoint(id){
if(!id) throw new ReferenceError("DocloopAdapter.getStoredEndpoint() missing id")
if(id._bsontype != 'ObjectID') id = ObjectId(id)
var endpoint_data = await this.endpoints.findOne({'_id': id})
if(!endpoint_data) throw new DocloopError("DocloopAdapter.getStoredEndpoint() unable to find Endpoint", 404)
var endpoint = this.newEndpoint(endpoint_data)
return endpoint
}
/**
* Creates a new instance of the endpoint class associated with this adapter. The new endpoint's adapter will be set to this adapter.
*
* @param {Object} data Data to instantiate the endpoint with.
*
* @return {DocloopEndpoint} Endpoint
*/
newEndpoint(data){
return new this.endpointClass(this, data)
}
/**
* This method is meant to be overwritten by a custom adapter class. Returns valid endpoints the current session has privileged access to.
* @async
*
* @abstract
*
* @param {SessionData} session_data Data of the current session associated with this adapter
*
* @return {ValidEndpoint[]}
*/
async getEndpoints(session_data){
throw new DocloopError("DocloopAdapter.getEndpoints not implemented for this adapter: "+this.id)
}
/**
* This method is meant to be overwritten by a custom adapter class. Retuns all endpoints stored in the db, that are also valid for the current session.
*
* @async
*
* @abstract
* @param {SessionData} session_data Data of the current session associated with this adapter
*
* @return {ValidEndpoint[]}
*/
async getStoredEndpoints(session_data){
throw new DocloopError("DocloopAdapter.getStoredEndpoints not implemented for this adapter: "+this.id)
}
/**
* Authorization data for client use. A truthy user value indicated that the session user is logged in with a third party service.
* @typedef {Object} AuthState
* @memberof DocloopAdapter
* @alias AuthState
*
* @property {String} [user=null] The username, login or id of the service the adapter makes use of
* @property {String} [link=null] Authorization Url. This is the url the client is supposed to open in order to login with the service this adapters want to make use of. Make sure to also add a route to the adapters sub app in order to catch the callback or webhook or wahever your service calls after the authorization.
*/
/**
* This method is meant to be overwritten by a custom adapter class. Returns the authorization state for the adapter in the current session.
*
* @async
*
* @abstract
*
* @param {SessionData} session_data Data of the current session associated with this adapter
*
* @return {AuthState}
*/
async getAuthState(session_data){
throw new DocloopError("DocloopAdapter.getAuthState not implemented for this adapter: "+this.id)
}
}
module.exports = DocloopAdapter
/**
* Either {@link DocloopAdapter} or any Class extending it.
*
* @typedef {Class} AdapterClass
* @memberof DocloopAdapter
* @alias AdapterClass
*/
/**
* An instance of either {@link DocloopAdapter} or any Class extending it.
*
* @typedef {Object} Adapter
* @memberof DocloopAdapter
* @alias Adapter
*/
/**
* TODO
*
* @typedef Annotation
* @memberof DocloopAdapter
* @alias Annotation
*/
/**
* TODO
*
* @event annotation
* @memberof DocloopAdapter
*
* @type {Annotation}
*/