Skip to content

Commit 4ec76bf

Browse files
authored
Get api routes from server:publicApi (#421)
Get API URLs list from the new `server:publicApi` route instead of the static routes file. If the protocol can not fetch the URLs from `server:publicApi`, there is some fallback: - if the route is not available for anonymous user: print warning message and use static routes - if the route does not exists (Kuzzle version < 1.9.0): get routes from `server:info` - if `server:info` is restricted: print warning message and use static routes
1 parent c5c45d4 commit 4ec76bf

File tree

12 files changed

+1787
-1569
lines changed

12 files changed

+1787
-1569
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ jobs:
8282

8383
before_script:
8484
- npm run doc-prepare
85-
- npm run --prefix doc/framework clone-repos
85+
- npm run --prefix doc/framework repositories -- clone
8686
script:
8787
- gem install typhoeus
8888
- HYDRA_MAX_CONCURRENCY=20 npm run --prefix doc/framework dead-links

doc/6/protocols/http/constructor/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ Http protocol connection options.
3131
| --------------- | -------------------------------- | ----------------------------------- |
3232
| `port` | <pre>number</pre><br/>(`7512`) | Kuzzle server port |
3333
| `sslConnection` | <pre>boolean</pre><br/>(`false`) | Use SSL to connect to Kuzzle server |
34+
| `customRoutes` | <pre>object</pre><br/>(`{}`) | Add custom routes <SinceBadge version="6.2.0"/> |
35+
36+
**Note:**
37+
38+
`customRoutes` are used to define plugins API routes or to overwrite existing API routes.
39+
They must have the following format:
40+
```js
41+
{
42+
'my-plugin/my-controller': {
43+
action: { verb: 'GET', url: '/some/url' },
44+
action2: { verb: 'GET', url: '/some/url/with/:parameter' }
45+
}
46+
}
47+
```
3448

3549
## Return
3650

doc/6/protocols/http/constructor/snippets/constructor.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ const
55
Http
66
} = require('kuzzle-sdk');
77

8+
const customRoutes = {
9+
'nyc-open-data-plugin/driver': {
10+
enroll: { verb: 'POST', url: '/_plugin/nyc-open-data-plugin/drivers' },
11+
remove: { verb: 'DELETE', url: '/_plugin/nyc-open-data-plugin/drivers/:driverId' }
12+
}
13+
};
14+
815
const options = {
16+
customRoutes,
917
sslConnection: false
1018
};
1119

doc/6/protocols/http/introduction/index.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,21 @@ order: 0
1010

1111
The Http protocol can be used by an instance of the SDK to communicate with your Kuzzle server.
1212

13-
:::info
13+
::: info
1414
This protocol does not allow to use the [real-time notifications](/sdk/js/6/essentials/realtime-notifications/).
1515

1616
If you need real-time features, then you have to use either [WebSocket](/sdk/js/6/protocols/websocket) or [SocketIO](/sdk/js/6/protocols/socketio) protocols.
1717
:::
18+
19+
## About HTTP routing
20+
21+
<SinceBadge version="6.2.0"/>
22+
23+
This protocol needs to build routes from the name of the controller and the action used. These routes are made available by Kuzzle via the [server:publicApi](/core/1/api/controllers/server/public-api) method or the [server:info](/core/1/api/controllers/server/info) method.
24+
25+
26+
<SinceBadge version="Kuzzle 1.9.0"/>
27+
For confidentiality reasons, it is preferable to expose only the `server:publicApi` route to the anonymous user.
28+
If this route is not available, the SDK will use the static definition of API routes that does not include routes developed in plugins.
29+
30+
Finally, it is also possible to manually define the routes to the actions of its plugins using the `customRoutes` option with the [Http protocol constructor](/sdk/js/6/protocols/http/constructor).

doc/6/protocols/http/properties/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ order: 10
1313
| -------------------- | -------- | ---------------------|
1414
| `connected` | <pre>boolean</pre> | Always returns `true` |
1515
| `host` | <pre>string</pre> | Kuzzle server host |
16-
| `http` | <pre>object</pre> | Returns a list of available routes |
16+
| `http` | <pre>object</pre> | Returns a list of available routes <DeprecatedBadge version="6.2.0"/> |
17+
| `routes` | <pre>object</pre> | Returns a list of available routes <SinceBadge version="6.2.0"/> |
1718
| `port` | <pre>number</pre> | Kuzzle server port |
1819
| `protocol` | <pre>string</pre> | `https` or `http` |
1920
| `ssl` | <pre>boolean</pre> | `true` if ssl is active |

doc/doc.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ case $1 in
4040
./framework/node_modules/.bin/vuepress build $DOC_VERSION/ $ARGS
4141
;;
4242

43+
build-netlify)
44+
export SITE_BASE="/"
45+
./framework/node_modules/.bin/vuepress build $DOC_VERSION/ $ARGS
46+
;;
47+
4348
upload)
4449
aws s3 sync $DOC_VERSION/.vuepress/dist s3://$S3_BUCKET$SITE_BASE --delete
4550
;;

features/features

sdk.html

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/protocols/http.js

Lines changed: 135 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use strict';
22

33
const
4-
KuzzleAbstractProtocol = require('./abstract/common'),
5-
_routes = require('./routes.json');
6-
4+
staticHttpRoutes = require('./routes.json'),
5+
KuzzleAbstractProtocol = require('./abstract/common');
76

87
class HttpWrapper extends KuzzleAbstractProtocol {
98
constructor(host, options = {}) {
@@ -13,20 +12,31 @@ class HttpWrapper extends KuzzleAbstractProtocol {
1312
throw new Error('host is required');
1413
}
1514

16-
// Application-side HTTP route overrides:
17-
if (options.http && options.http.customRoutes) {
18-
for (const controller in options.http.customRoutes) {
19-
if (options.http.customRoutes.hasOwnProperty(controller)) {
20-
this.http.routes[controller] = Object.assign(
21-
this.http.routes[controller] || {},
22-
options.http.customRoutes[controller]);
15+
this._routes = {};
16+
17+
this.customRoutes = options.customRoutes || {};
18+
19+
for (const [controller, definition] of Object.entries(this.customRoutes)) {
20+
for (const [action, route] of Object.entries(definition)) {
21+
if (!(typeof route.url === 'string' && route.url.length > 0)) {
22+
throw new Error(
23+
`Incorrect URL for custom route ${controller}:${action}.`);
24+
}
25+
if (!(typeof route.verb === 'string' && route.verb.length > 0)) {
26+
throw new Error(
27+
`Incorrect VERB for custom route ${controller}:${action}.`);
2328
}
2429
}
2530
}
2631
}
2732

33+
// @deprecated
2834
get http () {
29-
return _routes;
35+
return this.routes;
36+
}
37+
38+
get routes () {
39+
return this._routes;
3040
}
3141

3242
get protocol () {
@@ -38,15 +48,61 @@ class HttpWrapper extends KuzzleAbstractProtocol {
3848
}
3949

4050
/**
41-
* Connect to the websocket server
51+
* Connect to the server
4252
*/
4353
connect () {
4454
if (this.state === 'ready') {
4555
return Promise.resolve();
4656
}
4757

48-
return this._sendHttpRequest('GET', '/')
58+
return this._sendHttpRequest('GET', '/_publicApi')
59+
.then(({ result, error }) => {
60+
if (! error) {
61+
this._routes = this._constructRoutes(result);
62+
63+
return;
64+
}
65+
66+
if (error.status === 401 || error.status === 403) {
67+
this._warn('"server:publicApi" route is restricted for anonymous user.');
68+
this._warn('This route is used by the HTTP protocol to build API URLs.');
69+
this._warn('Fallback to static routes, some API routes may be unavailable as well as plugin custom routes');
70+
71+
// fallback to static http routes
72+
this._routes = staticHttpRoutes;
73+
74+
return;
75+
} else if (error.status === 404) {
76+
// fallback to server:info route
77+
// server:publicApi is only available since Kuzzle 1.9.0
78+
return this._sendHttpRequest('GET', '/')
79+
.then(({ result: res, error: err }) => {
80+
if (! err) {
81+
this._routes = this._constructRoutes(res.serverInfo.kuzzle.api.routes);
82+
this._staticRoutes = false;
83+
84+
return;
85+
}
86+
87+
if (err.status !== 401 && err.status !== 403) {
88+
throw err;
89+
}
90+
91+
this._warn('"server:info" route is restricted for anonymous user.');
92+
this._warn('This route is used by the HTTP protocol to build API URLs.');
93+
this._warn('If you want to expose your API routes without disclosing server information you can use "server:publicApi" (available in Kuzzle 1.9.0).');
94+
this._warn('Fallback to static routes, some API routes may be unavailable as well as plugin custom routes');
95+
96+
// fallback to static http routes
97+
this._routes = staticHttpRoutes;
98+
});
99+
}
100+
101+
throw error;
102+
})
49103
.then(() => {
104+
this._routes = Object.assign(this._routes, this.customRoutes);
105+
50106
// Client is ready
51107
this.clientConnected();
52108
})
@@ -101,12 +157,15 @@ class HttpWrapper extends KuzzleAbstractProtocol {
101157
}
102158
}
103159

104-
const
105-
route = this.http.routes[payload.controller] && this.http.routes[payload.controller][payload.action];
160+
const route = this.routes[payload.controller]
161+
&& this.routes[payload.controller][payload.action];
106162

107-
if (route === undefined) {
108-
const error = new Error(`No route found for ${payload.controller}/${payload.action}`);
163+
if (! route) {
164+
const error =
165+
new Error(`No URL found for "${payload.controller}:${payload.action}".`);
109166
this.emit(payload.requestId, {status: 400, error});
167+
168+
return;
110169
}
111170

112171
const
@@ -208,6 +267,65 @@ class HttpWrapper extends KuzzleAbstractProtocol {
208267
});
209268
}
210269

270+
_constructRoutes (publicApi) {
271+
const apiRoutes = Object.entries(publicApi)
272+
.reduce((routes, [controller, definition]) => {
273+
routes[controller] = {};
274+
275+
for (const [action, { http }] of Object.entries(definition)) {
276+
if (http && http.length === 1) {
277+
routes[controller][action] = http[0];
278+
} else if (http && http.length > 1) {
279+
routes[controller][action] = getCorrectRoute(http);
280+
}
281+
}
282+
283+
return routes;
284+
}, {});
285+
286+
for (const [controller, definition] of Object.entries(this.customRoutes)) {
287+
apiRoutes[controller] = definition;
288+
}
289+
290+
return apiRoutes;
291+
}
292+
293+
_warn (message) {
294+
console.warn(message); // eslint-disable-line no-console
295+
}
296+
297+
}
298+
299+
function getCorrectRoute (routes) {
300+
let
301+
shortestRoute = routes[0],
302+
postRoute,
303+
minLength = routes[0].url.length,
304+
sameLength = true;
305+
306+
for (const route of routes) {
307+
if (route.url.length !== minLength) {
308+
sameLength = false;
309+
}
310+
311+
if (route.url.length < minLength) {
312+
shortestRoute = route;
313+
minLength = route.url.length;
314+
}
315+
316+
if (route.verb === 'POST') {
317+
postRoute = route;
318+
}
319+
}
320+
321+
if (sameLength) {
322+
// with same URL size, we keep the POST route
323+
return postRoute;
324+
}
325+
326+
// with differents URL sizes, we keep the shortest because URL params
327+
// will be in the query string
328+
return shortestRoute;
211329
}
212330

213331
module.exports = HttpWrapper;

0 commit comments

Comments
 (0)