diff --git a/package-lock.json b/package-lock.json index 61f9f469..6d444062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,20 +19,20 @@ "dev": true }, "@babel/core": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", - "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", + "version": "7.16.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", + "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.16.7", + "@babel/generator": "^7.16.8", "@babel/helper-compilation-targets": "^7.16.7", "@babel/helper-module-transforms": "^7.16.7", "@babel/helpers": "^7.16.7", - "@babel/parser": "^7.16.7", + "@babel/parser": "^7.16.12", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.16.7", - "@babel/types": "^7.16.7", + "@babel/traverse": "^7.16.10", + "@babel/types": "^7.16.8", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -198,9 +198,9 @@ } }, "@babel/highlight": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", - "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", "requires": { "@babel/helper-validator-identifier": "^7.16.7", "chalk": "^2.0.0", @@ -254,9 +254,9 @@ } }, "@babel/parser": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", - "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==", + "version": "7.16.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz", + "integrity": "sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A==", "dev": true }, "@babel/template": { @@ -271,9 +271,9 @@ } }, "@babel/traverse": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.8.tgz", - "integrity": "sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ==", + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz", + "integrity": "sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", @@ -282,7 +282,7 @@ "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.16.8", + "@babel/parser": "^7.16.10", "@babel/types": "^7.16.8", "debug": "^4.1.0", "globals": "^11.1.0" @@ -795,9 +795,9 @@ "dev": true }, "@types/mocha": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", - "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", + "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", "dev": true }, "@types/morgan": { @@ -915,6 +915,13 @@ "requires": { "mime-types": "~2.1.24", "negotiator": "0.6.2" + }, + "dependencies": { + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + } } }, "acorn": { @@ -1290,9 +1297,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001300", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz", - "integrity": "sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA==", + "version": "1.0.30001301", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001301.tgz", + "integrity": "sha512-csfD/GpHMqgEL3V3uIgosvh+SVIQvCh43SNu9HRbP1lnxkKm1kjDG4f32PP571JplkLjfS+mg2p1gxR7MYrrIA==", "dev": true }, "chai": { @@ -1864,9 +1871,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { - "version": "1.4.48", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.48.tgz", - "integrity": "sha512-RT3SEmpv7XUA+tKXrZGudAWLDpa7f8qmhjcLaM6OD/ERxjQ/zAojT8/Vvo0BSzbArkElFZ1WyZ9FuwAYbkdBNA==", + "version": "1.4.51", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz", + "integrity": "sha512-JNEmcYl3mk1tGQmy0EvL5eik/CKSBuzAyGP0QFdG6LIgxQe3II0BL1m2zKc2MZMf3uGqHWE1TFddJML0RpjSHQ==", "dev": true }, "emoji-regex": { @@ -3750,9 +3757,9 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" }, "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "neo-async": { "version": "2.6.2", @@ -4480,12 +4487,12 @@ "dev": true }, "resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", "dev": true, "requires": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.8.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } diff --git a/package.json b/package.json index 10fa1747..41e6b47e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@types/chai-spies": "1.0.3", "@types/fs-extra": "9.0.13", "@types/json-schema": "7.0.9", - "@types/mocha": "9.0.0", + "@types/mocha": "9.1.0", "chai": "4.3.4", "chai-as-promised": "7.1.1", "chai-spies": "1.0.0", diff --git a/src/client.ts b/src/client.ts index daabc813..0b98b8a7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -15,9 +15,8 @@ import { SCAbstractRoute, SCErrorResponse, - SCFeedbackRequest, - SCFeedbackResponse, - SCFeedbackRoute, + SCFeatureConfiguration, + SCFeatureConfigurationPlugin, SCIndexRequest, SCIndexResponse, SCIndexRoute, @@ -32,17 +31,13 @@ import { SCSearchRoute, SCThings, } from '@openstapps/core'; -import {ApiError, CoreVersionIncompatibleError, OutOfRangeError} from './errors'; +import {ApiError, CoreVersionIncompatibleError, OutOfRangeError, PluginNotAvailableError} from './errors'; import {HttpClientHeaders, HttpClientInterface} from './http-client-interface'; /** * StApps-API client */ export class Client { - /** - * Instance of feedback request route - */ - private readonly feedbackRoute = new SCFeedbackRoute(); /** * Instance of index route @@ -59,6 +54,11 @@ export class Client { */ private readonly searchRoute = new SCSearchRoute(); + /** + * Features supported by backend + */ + private supportedFeatures?: SCFeatureConfiguration = undefined; + /** * Default headers * @@ -111,15 +111,6 @@ export class Client { } } - /** - * Send feedback - * - * @param feedback Feedback to send - */ - async feedback(feedback: SCFeedbackRequest): Promise { - return this.invokeRoute(this.feedbackRoute, undefined, feedback); - } - /** * Get a thing by its UID * @@ -157,10 +148,42 @@ export class Client { if (response.backend.SCVersion.split('.')[0] !== coreVersion.split('.')[0]) { throw new CoreVersionIncompatibleError(coreVersion, response.backend.SCVersion); } + /* istanbul ignore next */ + this.supportedFeatures = response?.app?.features; return response; } + /** + * Invoke a plugin route + * + * @param name name of the plugin + * @param parameters Parameters for the URL fragment + * @param body Body for the request + */ + async invokePlugin(name: string, + parameters?: { [k: string]: string; }, + body?: SCRequests): Promise { + + if (typeof this.supportedFeatures === 'undefined') { + const request: SCIndexRequest = {}; + const response = await this.invokeRoute(this.indexRoute, undefined, request); + if (typeof response?.app?.features !== 'undefined') { + /* istanbul ignore next */ + this.supportedFeatures = response?.app?.features; + } + } + const pluginInfo: SCFeatureConfigurationPlugin | undefined = this.supportedFeatures?.plugins?.[name]; + if (typeof pluginInfo === 'undefined') { + throw new PluginNotAvailableError(name); + } + + const route = new SCIndexRoute(); + route.urlPath = pluginInfo.urlPath; + + return this.invokeRoute(route, parameters, body); + } + /** * Invoke a route * @@ -177,7 +200,7 @@ export class Client { // TODO: remove headers headers: this.headers, method: route.method, - url: new URL(this.url + route.getUrlFragment(parameters)), + url: new URL(this.url + route.getUrlPath(parameters)), }); if (response.statusCode === route.statusCodeSuccess) { diff --git a/test/client.spec.ts b/test/client.spec.ts index 3507ae74..f62fa7da 100644 --- a/test/client.spec.ts +++ b/test/client.spec.ts @@ -13,9 +13,6 @@ * this program. If not, see . */ import { - SCFeedbackRequest, - SCFeedbackResponse, - SCFeedbackRoute, SCIndexResponse, SCIndexRoute, SCMessage, @@ -44,7 +41,6 @@ chai.use(chaiAsPromised); const sandbox = chai.spy.sandbox(); const indexRoute = new SCIndexRoute(); -const feedbackRoute = new SCFeedbackRoute(); const multiSearchRoute = new SCMultiSearchRoute(); const searchRoute = new SCSearchRoute(); @@ -65,6 +61,9 @@ export type RecursivePartial = { async function invokeIndexRoute(): Promise>> { return { body: { + app: { + features: {}, + }, backend: { SCVersion: 'foo.bar.dummy', }, @@ -113,58 +112,7 @@ export class ClientSpec { 'X-StApps-Version': 'foo.foo.foo', }, method: indexRoute.method, - url: new URL('http://localhost' + indexRoute.getUrlFragment()), - }); - } - - @test - async feedback() { - sandbox.on(httpClient, 'request', async (): Promise> => { - return { - body: {}, - headers: {}, - statusCode: feedbackRoute.statusCodeSuccess, - }; - }); - - expect(httpClient.request).not.to.have.been.first.called(); - - const client = new Client(httpClient, 'http://localhost'); - const feedback: SCFeedbackRequest = { - audiences: [ - 'employees', - ], - categories: [ - 'news' - ], - messageBody: 'Lorem ipsum.', - metaData: { - debug: true, - platform: 'android', - scope: {}, - sendable: true, - state: 'foo', - userAgent: 'bar', - version: 'foobar', - }, - name: 'foo', - origin: { - indexed: 'foo', - name: 'foo', - type: SCThingOriginType.Remote, - }, - type: SCThingType.Message, - uid: 'foo', - }; - await client.feedback(feedback); - - expect(httpClient.request).to.have.been.first.called.with({ - body: feedback, - headers: { - "Content-Type": "application/json", - }, - method: feedbackRoute.method, - url: new URL('http://localhost' + feedbackRoute.getUrlFragment()), + url: new URL('http://localhost' + indexRoute.getUrlPath()), }); } @@ -227,7 +175,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: searchRoute.method, - url: new URL('http://localhost' + searchRoute.getUrlFragment()), + url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } @@ -320,7 +268,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: indexRoute.method, - url: new URL('http://localhost' + indexRoute.getUrlFragment()), + url: new URL('http://localhost' + indexRoute.getUrlPath()), }); } @@ -335,6 +283,70 @@ export class ClientSpec { return client.handshake('bar.bar.dummy').should.be.rejectedWith(ApiError); } + @test + async invokePlugin() { + sandbox.on(httpClient, 'request', async(): Promise>> => { + return { + body: { + app: { + features: { + plugins: { + "supportedPlugin": { urlPath: "/" } + }, + }, + }, + }, + statusCode: indexRoute.statusCodeSuccess, + }; + }); + + expect(httpClient.request).not.to.have.been.first.called(); + const client = new Client(httpClient, 'http://localhost'); + + await client.invokePlugin('unsupportedPlugin').should.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); + + // again with cached feature definitions + return client.invokePlugin('supportedPlugin') + .should.not.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); + } + + @test + async invokePluginUnavailable() { + sandbox.on(httpClient, 'request', async(): Promise>> => { + return { + body: {}, + statusCode: indexRoute.statusCodeSuccess, + }; + }); + + expect(httpClient.request).not.to.have.been.first.called(); + + const client = new Client(httpClient, 'http://localhost'); + + await client.invokePlugin('supportedPlugin').should.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); + + sandbox.restore(); + sandbox.on(httpClient, 'request', async(): Promise>> => { + return { + body: { + app: { + features: { + plugins: { + 'unsupportedPlugin': { + urlPath: '/unsupported-plugin' + }, + }, + }, + }, + }, + statusCode: indexRoute.statusCodeSuccess, + }; + }); + // again with cached feature definitions + return client.invokePlugin('supportedPlugin') + .should.be.rejectedWith(ApiError,/.*supportedPlugin.*/gmi); + } + @test async invokeRoute() { sandbox.on(httpClient, 'request', invokeIndexRoute); @@ -350,7 +362,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: indexRoute.method, - url: new URL('http://localhost' + indexRoute.getUrlFragment()), + url: new URL('http://localhost' + indexRoute.getUrlPath()), }); } @@ -411,7 +423,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: multiSearchRoute.method, - url: new URL('http://localhost' + multiSearchRoute.getUrlFragment()), + url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), }); } @@ -461,7 +473,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: multiSearchRoute.method, - url: new URL('http://localhost' + multiSearchRoute.getUrlFragment()), + url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), }); expect(httpClient.request).to.have.been.second.called.with({ body: {foo: {size: 1000}, bar: {size: 500}, foobar: {size: 30}}, @@ -469,7 +481,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: multiSearchRoute.method, - url: new URL('http://localhost' + multiSearchRoute.getUrlFragment()), + url: new URL('http://localhost' + multiSearchRoute.getUrlPath()), }); } @@ -532,7 +544,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: searchRoute.method, - url: new URL('http://localhost' + searchRoute.getUrlFragment()), + url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } @@ -570,7 +582,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: searchRoute.method, - url: new URL('http://localhost' + searchRoute.getUrlFragment()), + url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } @@ -606,7 +618,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: searchRoute.method, - url: new URL('http://localhost' + searchRoute.getUrlFragment()), + url: new URL('http://localhost' + searchRoute.getUrlPath()), }); expect(httpClient.request).to.have.been.second.called.with({ body: {size: 1000}, @@ -614,7 +626,7 @@ export class ClientSpec { "Content-Type": "application/json", }, method: searchRoute.method, - url: new URL('http://localhost' + searchRoute.getUrlFragment()), + url: new URL('http://localhost' + searchRoute.getUrlPath()), }); } }