cocoda-sdk 1.0.13 → 2.0.0

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.
Files changed (36) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +74 -28
  3. package/dist/cjs/index.cjs +2673 -0
  4. package/dist/cocoda-sdk.js +33 -17423
  5. package/dist/cocoda-sdk.js.LICENSES.txt +105 -86
  6. package/dist/cocoda-sdk.js.map +7 -0
  7. package/dist/esm/errors/index.js +46 -0
  8. package/dist/esm/index.js +9 -0
  9. package/dist/esm/lib/CocodaSDK.js +269 -0
  10. package/dist/esm/providers/base-provider.js +368 -0
  11. package/dist/esm/providers/concept-api-provider.js +278 -0
  12. package/dist/esm/providers/index.js +20 -0
  13. package/dist/esm/providers/label-search-suggestion-provider.js +101 -0
  14. package/dist/esm/providers/loc-api-provider.js +185 -0
  15. package/dist/esm/providers/local-mappings-provider.js +337 -0
  16. package/dist/esm/providers/mappings-api-provider.js +264 -0
  17. package/dist/esm/providers/occurrences-api-provider.js +163 -0
  18. package/dist/esm/providers/reconciliation-api-provider.js +140 -0
  19. package/dist/esm/providers/skosmos-api-provider.js +345 -0
  20. package/{utils → dist/esm/utils}/index.js +40 -53
  21. package/dist/esm/utils/lodash.js +34 -0
  22. package/package.json +16 -17
  23. package/errors/index.js +0 -119
  24. package/index.js +0 -5
  25. package/lib/CocodaSDK.js +0 -360
  26. package/providers/base-provider.js +0 -581
  27. package/providers/concept-api-provider.js +0 -377
  28. package/providers/index.js +0 -34
  29. package/providers/label-search-suggestion-provider.js +0 -219
  30. package/providers/loc-api-provider.js +0 -275
  31. package/providers/local-mappings-provider.js +0 -459
  32. package/providers/mappings-api-provider.js +0 -396
  33. package/providers/occurrences-api-provider.js +0 -234
  34. package/providers/reconciliation-api-provider.js +0 -211
  35. package/providers/skosmos-api-provider.js +0 -441
  36. package/utils/lodash.js +0 -21
@@ -1,581 +0,0 @@
1
- const jskos = require("jskos-tools")
2
- const _ = require("../utils/lodash")
3
- const axios = require("axios")
4
- const utils = require("../utils")
5
- const errors = require("../errors")
6
-
7
- /**
8
- * BaseProvider to be subclassed to implement specific providers. Do not initialize a registry directly with this!
9
- *
10
- * Prefix all internal method and properties with underscore (e.g. `this._cache`)!
11
- *
12
- * Methods that can be overridden:
13
- * - Do not override the constructor! Use _prepare or _setup instead.
14
- * - _prepare: will be called before the registry is initialized (i.e. it's `/status` endpoint is queries if necessasry)
15
- * - _setup: will be called after registry is initialized (i.e. it's `/status` endpoint is queries if necessasry), should be used to set properties on this.has and custom preparations
16
- * - isAuthorizedFor: override if you want to customize
17
- * - supportsScheme: override if you want to customize
18
- * - setRegistries: implement this method if the provider needs access to other registries in cocoda-sdk (takes one parameter `registries`)
19
- * - getRegistries
20
- * - getSchemes
21
- * - getTypes
22
- * - suggest
23
- * - getConcordances
24
- * - getOccurrences
25
- * - getTop
26
- * - getConcepts
27
- * - getNarrower
28
- * - getAncestors
29
- * - search
30
- * - getMapping
31
- * - getMappings
32
- * - postMapping
33
- * - postMappings
34
- * - putMapping
35
- * - patchMapping
36
- * - deleteMapping
37
- * - deleteMappings
38
- * - getAnnotations
39
- * - postAnnotation
40
- * - putAnnotation
41
- * - patchAnnotation
42
- * - deleteAnnotation
43
- *
44
- * Internal (starting with underscore) and external properties that can be used:
45
- * - `this.has`: an object of functionality of the registry (needs to be set by subclasses)
46
- * - `this.languages`: an array of language tags provided by the user in order of priority
47
- * - `this._jskos`: the raw JSKOS object used to initialize this registry
48
- * - `this._path`: if available, the path of the current browser window
49
- * - `this._defaultLanguages`: an array of default language tags
50
- * - `this._auth`: authentication key and token
51
- * - `this._api`: object of API endpoints for the registry
52
- * - `this._config`: configuration of the registry as provided by the `/status` endpoint if available
53
- *
54
- * All of the request methods take ONE parameter which is a config object. Actual parameters should be properties on this object. The config object should be destructured to remove the properties your method needs, and the remaining config object should be given to the axios request.
55
- * Example:
56
- * ```js
57
- * getConcept({ concept, ...config }) {
58
- * return this.axios({
59
- * ...config,
60
- * method: "get",
61
- * params: {
62
- * uri: concept.uri,
63
- * },
64
- * })
65
- * }
66
- * ```
67
- *
68
- * Always use `this.axios` like in the example for http requests!
69
- *
70
- * @category Providers
71
- */
72
- class BaseProvider {
73
-
74
- /**
75
- * Provider constructor.
76
- *
77
- * @param {Object} registry the registry for this provider
78
- */
79
- constructor(registry = {}) {
80
- this._jskos = registry
81
-
82
- this.axios = axios.create({
83
- // TODO: Decide on timeout value
84
- timeout: 20000,
85
- })
86
- // Path is used for https check and local mappings
87
- this._path = typeof window !== "undefined" && window.location.pathname
88
- /**
89
- * A dictionary with functionality of the registry (e.g. `registry.has.schemes`).
90
- * @type {Object}
91
- * @readonly
92
- */
93
- this.has = {}
94
- // Set default language priority list
95
- this._defaultLanguages = "en,de,fr,es,nl,it,fi,pl,ru,cs,jp".split(",")
96
- /**
97
- * A list of RFC 3066 language tags in lowercase in order of priority.
98
- * @type {string[]}
99
- */
100
- this.languages = []
101
- // Set auth details to null
102
- this._auth = {
103
- key: null,
104
- bearerToken: null,
105
- }
106
- // Set repeating requests array
107
- this._repeating = []
108
-
109
- // Set API URLs from registry object
110
- this._api = {
111
- status: registry.status,
112
- schemes: registry.schemes,
113
- top: registry.top,
114
- data: registry.data,
115
- concepts: registry.concepts,
116
- narrower: registry.narrower,
117
- ancestors: registry.ancestors,
118
- types: registry.types,
119
- suggest: registry.suggest,
120
- search: registry.search,
121
- "voc-suggest": registry["voc-suggest"],
122
- "voc-search": registry["voc-search"],
123
- mappings: registry.mappings,
124
- concordances: registry.concordances,
125
- annotations: registry.annotations,
126
- occurrences: registry.occurrences,
127
- reconcile: registry.reconcile,
128
- api: registry.api,
129
- }
130
- this._config = {}
131
-
132
- // Set default retry config
133
- this.setRetryConfig()
134
-
135
- // Add a request interceptor
136
- this.axios.interceptors.request.use((config) => {
137
- // Add language parameter to request
138
- const language = _.uniq([].concat(_.get(config, "params.language", "").split(","), this.languages, this._defaultLanguages).filter(lang => lang != "")).join(",")
139
- _.set(config, "params.language", language)
140
- // Set auth
141
- if (this.has.auth && this._auth.bearerToken && !_.get(config, "headers.Authorization")) {
142
- _.set(config, "headers.Authorization", `Bearer ${this._auth.bearerToken}`)
143
- }
144
-
145
- // Don't perform http requests if site is used via https
146
- if (config.url.startsWith("http:") && typeof window !== "undefined" && window.location.protocol == "https:") {
147
- // TODO: Return proper error object.
148
- throw new axios.Cancel("Can't call http API from https.")
149
- }
150
-
151
- return config
152
- })
153
-
154
- // Add a response interceptor
155
- this.axios.interceptors.response.use(({ data, headers = {}, config = {} }) => {
156
- // Apply unicode normalization
157
- data = jskos.normalize(data)
158
-
159
- // Add URL to array as prop
160
- let url = config.url
161
- if (!url.endsWith("?")) {
162
- url += "?"
163
- }
164
- _.forOwn(config.params || {}, (value, key) => {
165
- url += `${key}=${encodeURIComponent(value)}&`
166
- })
167
-
168
- if (_.isArray(data) || _.isObject(data)) {
169
- // Add total count to array as prop
170
- let totalCount = parseInt(headers["x-total-count"])
171
- if (!isNaN(totalCount)) {
172
- data._totalCount = totalCount
173
- }
174
- data._url = url
175
- }
176
-
177
- // TODO: Return data or whole response here?
178
- return data
179
- }, error => {
180
- const count = _.get(error, "config._retryCount", 0)
181
- const method = _.get(error, "config.method")
182
- const statusCode = _.get(error, "response.status")
183
- if (
184
- this._retryConfig.methods.includes(method)
185
- && this._retryConfig.statusCodes.includes(statusCode)
186
- && count < this._retryConfig.count
187
- ) {
188
- error.config._retryCount = count + 1
189
- // from: https://github.com/axios/axios/issues/934#issuecomment-531463172
190
- if(error.config.data) error.config.data = JSON.parse(error.config.data)
191
- return new Promise((resolve, reject) => {
192
- setTimeout(() => {
193
- this.axios(error.config).then(resolve).catch(reject)
194
- }, (() => {
195
- const delay = this._retryConfig.delay
196
- if (typeof delay === "function") {
197
- return delay(count)
198
- }
199
- return delay
200
- })())
201
- })
202
- } else {
203
- return Promise.reject(error)
204
- }
205
- })
206
-
207
- const currentRequests = []
208
- for (let { method, type } of utils.requestMethods) {
209
- // Make sure all methods exist, but thrown an error if they are not implemented
210
- const existingMethod = this[method] && this[method].bind(this)
211
- if (!existingMethod) {
212
- this[method] = () => { throw new errors.MethodNotImplementedError({ method }) }
213
- continue
214
- }
215
- this[method] = (options = {}) => {
216
- // Allow calling the "raw" method without adjustments
217
- if (options._raw) {
218
- delete options._raw
219
- return existingMethod(options)
220
- }
221
- // Return from existing requests if one exists
222
- const existingRequest = currentRequests.find(r => r.method == method && _.isEqual(r.options, options))
223
- if (existingRequest) {
224
- return existingRequest.promise
225
- }
226
- // Add an axios cancel token to each request
227
- let source
228
- if (!options.cancelToken) {
229
- source = this.getCancelTokenSource()
230
- options.cancelToken = source.token
231
- }
232
- // Make sure a registry is initialized (see `init` method) before any request
233
- // TODO: Is this a good solution?
234
- const promise = this.init()
235
- .then(() => existingMethod(options))
236
- // Add totalCount to arrays
237
- .then(result => {
238
- if (_.isArray(result) && result._totalCount === undefined) {
239
- result._totalCount = result.length
240
- } else if (_.isObject(result) && result._totalCount === undefined) {
241
- result._totalCount = 1
242
- }
243
- if (result && type && this[`adjust${type}`]) {
244
- result = this[`adjust${type}`](result)
245
- }
246
- return result
247
- }).catch(error => {
248
- if (error instanceof errors.CDKError) {
249
- throw error
250
- } else {
251
- if (error.response) {
252
- // 4xx = invalid request
253
- if (error.response.status.toString().startsWith(4)) {
254
- throw new errors.InvalidRequestError({ relatedError: error, code: error.response.status })
255
- } else {
256
- throw new errors.BackendError({ relatedError: error, code: error.response.status })
257
- }
258
- } else if (error.request) {
259
- if (typeof navigator !== "undefined") {
260
- // If connected, it should be a backend problem
261
- if (navigator.connection || navigator.mozConnection || navigator.webkitConnection) {
262
- throw new errors.BackendUnavailableError({ relatedError: error })
263
- }
264
- }
265
- // Otherwise, assume a network error
266
- throw new errors.NetworkError({ relatedError: error })
267
- } else {
268
- // Otherwise, throw generic CDKError
269
- throw new errors.CDKError({ relatedError: error })
270
- }
271
- }
272
- })
273
- // Attach cancel method to Promise
274
- if (source) {
275
- promise.cancel = (...args) => {
276
- return source.cancel(...args)
277
- }
278
- }
279
- // Save to list of existing requests
280
- const request = {
281
- method,
282
- options: _.omit(options, ["cancelToken"]),
283
- promise,
284
- }
285
- currentRequests.push(request)
286
- // Remove from list of current requests after promise is done
287
- promise.catch(() => {}).then(() => currentRequests.splice(currentRequests.indexOf(request), 1))
288
- // Add adjustment methods
289
- return promise
290
- }
291
- }
292
- }
293
-
294
- // Expose some properties from original registry object as getters
295
- get uri() { return this._jskos.uri }
296
- get notation() { return this._jskos.notation }
297
- get prefLabel() { return this._jskos.prefLabel }
298
- get definition() { return this._jskos.definition }
299
- get schemes() { return this._jskos.schemes }
300
- get excludedSchemes() { return this._jskos.excludedSchemes }
301
- get stored() { return this._jskos.stored !== undefined ? this._jskos.stored : this.constructor.stored }
302
-
303
- /**
304
- * Load data about registry via the status endpoint.
305
- *
306
- * @returns {Promise} Promise that resolves when initialization is complete.
307
- */
308
- async init() {
309
- // Save the actual Promise in _init and return it immediately on a second call
310
- if (this._init) {
311
- return this._init
312
- }
313
- this._init = (async () => {
314
- // Call preparation method
315
- this._prepare()
316
- let status
317
- if (_.isString(this._api.status)) {
318
- // Request status endpoint
319
- try {
320
- status = await this.axios({
321
- method: "get",
322
- url: this._api.status,
323
- })
324
- } catch(error) {
325
- if (_.get(error, "response.status") === 404) {
326
- // If /status is not available, remove from _api
327
- this._api.status = null
328
- }
329
- }
330
- } else {
331
- // Assume object
332
- status = this._api.status
333
- }
334
- if (_.isObject(status) && !_.isEmpty(status)) {
335
- // Set config
336
- this._config = status.config || {}
337
- // Merge status result and existing API URLs
338
- for (let key of Object.keys(this._api)) {
339
- // Only override if undefined
340
- if (this._api[key] === undefined) {
341
- // Fall back to null, i.e. if /status was successful, no endpoints are implied by the provider
342
- // See also: https://github.com/gbv/cocoda-sdk/issues/21
343
- this._api[key] = status[key] || null
344
- }
345
- }
346
- }
347
- this._setup()
348
- })()
349
- return this._init
350
- }
351
-
352
- /**
353
- * Preparation to be executed before init. Should be overwritten by subclasses.
354
- *
355
- * @private
356
- */
357
- _prepare() {}
358
-
359
- /**
360
- * Setup to be executed after init. Should be overwritten by subclasses.
361
- *
362
- * @private
363
- */
364
- _setup() {}
365
-
366
- /**
367
- * Returns a source for a axios cancel token.
368
- *
369
- * @returns {Object} axios cancel token source
370
- */
371
- getCancelTokenSource() {
372
- return axios.CancelToken.source()
373
- }
374
-
375
- /**
376
- * Sets authentication credentials.
377
- *
378
- * @param {Object} options
379
- * @param {string} options.key public key of login-server instance the user is authorized for
380
- * @param {string} options.bearerToken token that is sent with each request
381
- */
382
- setAuth({ key = this._auth.key, bearerToken = this._auth.bearerToken }) {
383
- this._auth.key = key
384
- this._auth.bearerToken = bearerToken
385
- }
386
-
387
- /**
388
- * Sets retry configuration.
389
- *
390
- * @param {Object} config
391
- * @param {string[]} [config.methods=["get", "head", "options"]] HTTP methods to retry (lowercase)
392
- * @param {number[]} [config.statusCodes=[401, 403]] status codes to retry
393
- * @param {number} [config.count=3] maximum number of retries
394
- * @param {number|Function} [config.delay=300*count] a delay in ms or a function that takes the number of current retries and returns a delay in ms
395
- */
396
- setRetryConfig(config = {}) {
397
- this._retryConfig = Object.assign({
398
- methods: ["get", "head", "options"],
399
- statusCodes: [401, 403],
400
- count: 3,
401
- delay: (count) => {
402
- return 300 * count
403
- },
404
- }, config)
405
- }
406
-
407
- /**
408
- * Returns whether a user is authorized for a certain request.
409
- *
410
- * @param {Object} options
411
- * @param {string} options.type type of item (e.g. mappings)
412
- * @param {string} options.action action to be performed (read/create/update/delete)
413
- * @param {Object} options.user user object
414
- * @param {boolean} [options.crossUser] whether the request is a crossUser request (i.e. updading/deleting another user's item)
415
- * @returns {boolean}
416
- */
417
- isAuthorizedFor({ type, action, user, crossUser }) {
418
- if (action == "read" && this.has[type] === true) {
419
- return true
420
- }
421
- if (!this.has[type]) {
422
- return false
423
- }
424
- const options = _.get(this._config, `${type}.${action}`)
425
- if (!options) {
426
- return !!this.has[type][action]
427
- }
428
- if (options.auth && (!user || !this._auth.key)) {
429
- return false
430
- }
431
- // Public key mismatch
432
- if (options.auth && this._auth.key != _.get(this._config, "auth.key")) {
433
- return false
434
- }
435
- if (options.auth && options.identities) {
436
- // Check if one of the user's identities matches
437
- const uris = [user.uri].concat(Object.values(user.identities || {}).map(id => id.uri)).filter(uri => uri != null)
438
- if (_.intersection(uris, options.identities).length == 0) {
439
- return false
440
- }
441
- }
442
- if (options.auth && options.identityProviders) {
443
- // Check if user has the required provider
444
- const providers = Object.keys((user && user.identities) || {})
445
- if (_.intersection(providers, options.identityProviders).length == 0) {
446
- return false
447
- }
448
- }
449
- if (crossUser) {
450
- return !!options.crossUser
451
- }
452
- return !!this.has[type][action]
453
- }
454
-
455
- /**
456
- * Returns a boolean whether a certain target scheme is supported or not.
457
- *
458
- * @param {Object} scheme
459
- * @returns {boolean}
460
- */
461
- supportsScheme(scheme) {
462
- if (!scheme) {
463
- return false
464
- }
465
- let schemes = _.isArray(this.schemes) ? this.schemes : null
466
- if (schemes == null && !jskos.isContainedIn(scheme, this.excludedSchemes || [])) {
467
- return true
468
- }
469
- return jskos.isContainedIn(scheme, schemes)
470
- }
471
-
472
- adjustConcept(concept) {
473
- // Add _getNarrower function to concepts
474
- concept._getNarrower = (config) => {
475
- return this.getNarrower({ ...config, concept })
476
- }
477
- // Add _getAncestors function to concepts
478
- concept._getAncestors = (config) => {
479
- return this.getAncestors({ ...config, concept })
480
- }
481
- // Add _getDetails function to concepts
482
- concept._getDetails = async (config) => {
483
- return (await this.getConcepts({ ...config, concepts: [concept] }))[0]
484
- }
485
- // Adjust broader/narrower/ancestors if necessary
486
- for (let type of ["broader", "narrower", "ancestors"]) {
487
- if (Array.isArray(concept[type]) && !concept[type].includes(null)) {
488
- concept[type] = this.adjustConcepts(concept[type])
489
- }
490
- }
491
- // Add _registry to concepts
492
- concept._registry = this
493
- return concept
494
- }
495
- adjustConcepts(concepts) {
496
- return utils.withCustomProps(concepts.map(concept => this.adjustConcept(concept)), concepts)
497
- }
498
- adjustRegistries(registries) {
499
- return registries
500
- }
501
- adjustSchemes(schemes) {
502
- for (let scheme of schemes) {
503
- // Add _getTop function to schemes
504
- scheme._getTop = (config) => {
505
- return this.getTop({ ...config, scheme })
506
- }
507
- // Add _getTypes function to schemes
508
- scheme._getTypes = (config) => {
509
- return this.getTypes({ ...config, scheme })
510
- }
511
- // Add _registry to schemes
512
- scheme._registry = this
513
- // Add _suggest function to schemes
514
- scheme._suggest = ({ search, ...config }) => {
515
- return this.suggest({ ...config, search, scheme })
516
- }
517
- }
518
- return schemes
519
- }
520
- adjustConcordances(concordances) {
521
- for (let concordance of concordances) {
522
- // Add _registry to concordance
523
- concordance._registry = this
524
- }
525
- return concordances
526
- }
527
- adjustMapping(mapping) {
528
- // TODO: Add default type
529
- // Add fromScheme and toScheme if missing
530
- for (let side of ["from", "to"]) {
531
- let sideScheme = `${side}Scheme`
532
- if (!mapping[sideScheme]) {
533
- mapping[sideScheme] = _.get(jskos.conceptsOfMapping(mapping, side), "[0].inScheme[0]", null)
534
- }
535
- }
536
- mapping._registry = this
537
- if (!mapping.identifier) {
538
- // Add mapping identifiers for this mapping
539
- let identifier = _.get(jskos.addMappingIdentifiers(mapping), "identifier")
540
- if (identifier) {
541
- mapping.identifier = identifier
542
- }
543
- }
544
- return mapping
545
- }
546
- adjustMappings(mappings) {
547
- return utils.withCustomProps(mappings.map(mapping => this.adjustMapping(mapping)), mappings)
548
- }
549
-
550
- /**
551
- * POSTs multiple mappings. Do not override in subclass!
552
- *
553
- * @param {Object} config
554
- * @param {Array} config.mappings array of mapping objects
555
- * @returns {Object[]} array of created mapping objects
556
- */
557
- async postMappings({ mappings, ...config } = {}) {
558
- if (!mappings || !mappings.length) {
559
- throw new errors.InvalidOrMissingParameterError({ parameter: "mappings" })
560
- }
561
- return Promise.all(mappings.map(mapping => this.postMapping({ mapping, ...config, _raw: true })))
562
- }
563
-
564
- /**
565
- * DELETEs multiple mappings. Do not override in subclass!
566
- *
567
- * @param {Object} config
568
- * @param {Array} config.mappings array of mapping objects
569
- */
570
- async deleteMappings({ mappings, ...config } = {}) {
571
- if (!mappings || !mappings.length) {
572
- throw new errors.InvalidOrMissingParameterError({ parameter: "mappings" })
573
- }
574
- return Promise.all(mappings.map(mapping => this.deleteMapping({ mapping, ...config, _raw: true })))
575
- }
576
-
577
- }
578
-
579
- BaseProvider.providerName = "Base"
580
-
581
- module.exports = BaseProvider