-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocloop-link.js
More file actions
238 lines (181 loc) · 6.85 KB
/
docloop-link.js
File metadata and controls
238 lines (181 loc) · 6.85 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
'use strict'
var ObjectId = require('mongodb').ObjectID,
Promise = require('bluebird'),
DocloopError = require('./docloop-error-handling.js').DocloopError
//TODO: Tests
//TODO: Decor?
/**
* Class representing a link between a source and a target.
*
* @alias DocloopLink
*
* @memberof module:docloop
*
* @param {DocloopCore} core The core system. Since core is a factory for Links, this param will usually be set automatically.
* @param {Object} data
* @param {String|bson} [data.id] Link id.
* @param {String|bson} [data._id] If data.id is not present _id will be used. This is handy, if the data comes directly form te database.
* @param {EndpointData} data.source
* @param {EndpointData} data.target
*
* @property {bson} id
* @property {Endpoint} source An endpoint representing a source of annotations
* @property {Endpoint} target An endpoint representing a target resource for issues
* @property {LinkData} export Getter
* @property {LinkSkeleton} skeleton Getter
*
* @throws {ReferenceError} If core is missing .
* @throws {TypeError} If core is not an instance of DocloopCore.
* @throws {ReferenceError} If data is missing .
*/
class DocloopLink {
constructor(core, data){
if(core === undefined ) throw new ReferenceError("Link.constructor() missing core")
if(core === null || core.constructor.name != 'DocloopCore')
throw new TypeError("Link.constructor() expected core to be instance of DocloopCore got "+core)
if(!data) throw new ReferenceError("Link.constructor() missing data")
this.core = core
this.id = data.id || data._id || undefined
if(this.id && (this.id._bsontype != 'ObjectID') ) this.id = ObjectId(this.id)
this.importData(data)
}
/**
* Imports source and target data. Mainly used by the constructor.
*
* @param {Object} data
* @param {EndpointData} source Source endpoint data as used in the {@link DocloopEndpoint#constructor}
* @param {EndpointData} source Target endpoint data as used in the {@link DocloopEndpoint#constructor}
*
* returns {this}
*/
importData({source, target} = {}){
if(source === undefined) throw new ReferenceError("Link.constructor() missing source")
if(target === undefined) throw new ReferenceError("Link.constructor() missing target")
if(source.identifier === undefined) throw new ReferenceError("Link.importData() missing source identifier")
if(target.identifier === undefined) throw new ReferenceError("Link.importData() missing target identifier")
var source_adapter = this.core.adapters[source.identifier.adapter],
target_adapter = this.core.adapters[target.identifier.adapter]
if(!source_adapter) throw new DocloopError("Link.importData() no matching source adapter found")
if(!target_adapter) throw new DocloopError("Link.importData() no matching target adapter found")
this.source = source_adapter.newEndpoint(source)
this.target = target_adapter.newEndpoint(target)
return this
}
/**
* @typedef {Object} LinkData
* @alias LinkData
*
* @memberof DocloopLink
*
* @property {bson} [id] Link id
* @property {EndpointData} source Source data
* @property {EndpointData} target Target data
*/
get export(){
return {
id: this.id,
source: this.source.export,
target: this.target.export,
}
}
/**
* @typedef {Object} LinkSkeleton
* @alias LinkSkeleton
*
* @memberof DocloopLink
*
* @property {bson} id Link id
* @property {EndpointSkeleton} source Source skeleton
* @property {EndpointSkeleton} target Target skeleton
*/
get skeleton(){
return {
id: this.id || undefined,
source: this.source.skeleton,
target: this.target.skeleton
}
}
/**
* Check if there already exists a link with the same source identifier and the same target identifier as the link at hand. If so throws and error.
*
* @async
*
* @return {this} for chaining
*
* @throws {DocloopError} If duplicate exists
*/
async preventDuplicate(){
var sources = await this.source.adapter.endpoints.find({identifier: this.source.identifier}).toArray(),
targets = await this.target.adapter.endpoints.find({identifier: this.target.identifier}).toArray()
if(sources.length == 0) return this
if(targets.length == 0) return this
var source_queries = sources.map( source => ({ "source.id" : source._id, "source.adapter": source.identifier.adapter}) ),
target_queries = targets.map( target => ({ "target.id" : target._id, "target.adapter": target.identifier.adapter}) ),
duplicates = await this.core.links.find({
"$and":[
{"$or": source_queries },
{"$or": target_queries }
]
}).toArray()
if(duplicates.length > 0) throw new DocloopError("Link.preventDuplicate() duplicate found", 409)
return this
}
//TODO: refuse doble store?? Also double store for source and target?
/**
* Stores the link to the database as new document. First it stores its source and target, then stores a new document using the data from {@link DocLLoopLink#.skeleton}
*
* @async
*
* @return {bson} The mongo-db id for the inserted document.
*/
async store() {
var [source_id, target_id] = await Promise.all([
this.source.store(),
this.target.store()
]),
result = await this.core.links.insertOne(this.skeleton)
//TODO: Error result
this.id = result.insertedId
return this.id
}
/**
* Updates source and target using {@link DocloopEndpoint#update}
*
* @async
*/
async update(){
await this.source.update()
await this.target.update()
}
/**
* Removes the link from the database. First it removes source and target from the database, then the actual link document.
*
* @async
*
* @return {undefined}
*
* @throws {ReferenceError} If this.id is missing. This happens most likely if the link had not been stored.
*/
async remove() {
if(!this.id) throw new ReferenceError("Link.remove() missing id")
await this.source.remove()
await this.target.remove()
var deletion = await this.core.links.deleteOne({_id: this.id})
if(!deletion || !deletion.result || deletion.result.n != 1)
throw new DocloopError("Link.remove() db failed to remove link")
}
/**
* Checks if the current session has acces to source and target with {@link Endpoint#_validate}
*
* @param {session} Express session
*
* @return {undefined}
*/
async _validate(session){
try{ await this.source._validate(session) }
catch(e){ throw new DocloopError("Link._validate() unable to validate source "+e, e && e.status) }
try{ await this.target._validate(session) }
catch(e){ throw new DocloopError("Link._validate() unable to validate target "+e, e && e.status) }
}
}
module.exports = DocloopLink