UNPKG

kuzzle-sdk

Version:
398 lines 15.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Base_1 = require("./abstract/Base"); const routes_json_1 = __importDefault(require("./routes.json")); /** * Http protocol used to connect to a Kuzzle server. * * The Http protocol cannot use the realtime capabilities of Kuzzle. */ class HttpProtocol extends Base_1.KuzzleAbstractProtocol { /** * @param host Kuzzle server hostname or IP * @param options Http connection options * - `customRoutes` Add custom routes * - `headers` Default headers sent with each HTTP request (default: `{}`) * - `port` Kuzzle server port (default: `7512`) * - `ssl` Use SSL to connect to Kuzzle server. Default `false` unless port is 443 or 7443. * - `timeout` Connection timeout in milliseconds (default: `0`) */ constructor(host, options = {}) { super(host, options, "http"); if (typeof host !== "string" || host === "") { throw new Error("host is required"); } this._routes = {}; this._timeout = options.timeout || 0; this._customRoutes = options.customRoutes || {}; this._defaultHeaders = options.headers || {}; for (const controller of Object.keys(this._customRoutes)) { const definition = this._customRoutes[controller]; for (const action of Object.keys(definition)) { const route = definition[action]; if (!(typeof route.url === "string" && route.url.length > 0)) { throw new Error(`Incorrect URL for custom route ${controller}:${action}.`); } if (!(typeof route.verb === "string" && route.verb.length > 0)) { throw new Error(`Incorrect VERB for custom route ${controller}:${action}.`); } } } } /** * @deprecated Use `routes` instead */ get http() { return this.routes; } /** * Returns a list of available routes */ get routes() { return this._routes; } /** * `http` or `https` */ get protocol() { return this.ssl ? "https" : "http"; } /** * Always returns `true` */ get connected() { return true; } /** * Connection timeout in milliseconds */ get timeout() { return this._timeout; } set timeout(timeout) { this._timeout = timeout; } /** * Connect to the server */ connect() { if (this.state === "ready") { return Promise.resolve(); } const publicApiRequest = { method: "GET", path: "/_publicApi", payload: { headers: this._defaultHeaders, }, }; return this._sendHttpRequest(publicApiRequest) .then(({ result, error }) => { if (!error) { this._routes = this._constructRoutes(result); return; } if (error.status === 401 || error.status === 403) { this._warn('"server:publicApi" route is restricted for anonymous user.'); this._warn("This route is used by the HTTP protocol to build API URLs."); this._warn("Fallback to static routes, some API routes may be unavailable as well as plugin custom routes"); // fallback to static http routes this._routes = routes_json_1.default; return; } else if (error.status === 404) { // fallback to server:info route // server:publicApi is only available since Kuzzle 1.9.0 const serverInfoRequest = { method: "GET", path: "/", payload: { headers: this._defaultHeaders, }, }; return this._sendHttpRequest(serverInfoRequest).then(({ result: res, error: err }) => { if (!err) { this._routes = this._constructRoutes(res.serverInfo.kuzzle.api.routes); return; } if (err.status !== 401 && err.status !== 403) { throw err; } this._warn('"server:info" route is restricted for anonymous user.'); this._warn("This route is used by the HTTP protocol to build API URLs."); 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).'); this._warn("Fallback to static routes, some API routes may be unavailable as well as plugin custom routes"); // fallback to static http routes this._routes = routes_json_1.default; }); } throw error; }) .then(() => { this._routes = Object.assign(this._routes, this._customRoutes); // Client is ready this.clientConnected(); }) .catch((err) => { const connectionError = new Error(`Unable to connect to kuzzle server at ${this.host}:${this.port}`); connectionError.internal = err; this.emit("networkError", connectionError); throw err; }); } /** * Enable cookie authentication support at protocol level */ enableCookieSupport() { if (typeof XMLHttpRequest === "undefined") { throw new Error("Support for cookie cannot be enabled outside of a browser"); } super.enableCookieSupport(); } /** * Preprocess and format the request * * @param {Object} data * @returns {Promise<any>} */ formatRequest(request, options = {}) { const route = this.routes[request.controller] && this.routes[request.controller][request.action]; if (!route) { const error = new Error(`No URL found for "${request.controller}:${request.action}".`); this.emit(request.requestId, { error, status: 400 }); return; } const method = options.verb || route.verb; const payload = { action: undefined, body: undefined, collection: undefined, controller: undefined, headers: { "Content-Type": "application/json", ...this._defaultHeaders, }, index: undefined, meta: undefined, requestId: undefined, }; const queryArgs = {}; for (const key of Object.keys(request)) { const value = request[key]; if (key === "body") { if (method === "GET") { Object.assign(queryArgs, value); } else { payload.body = JSON.stringify(value); } } else if (key === "jwt") { payload.headers.authorization = "Bearer " + value; } else if (key === "volatile") { payload.headers["x-kuzzle-volatile"] = JSON.stringify(value); } else if (key === "index" || key === "collection") { // If we're calling a non-native route that answer to a GET request // we need to add the index and collection (if provided) to the query string if (!routes_json_1.default[request.controller] && method === "GET") { queryArgs[key] = value; } else { payload[key] = value; } } else if (Object.prototype.hasOwnProperty.call(payload, key)) { payload[key] = value; } else if (value !== undefined && value !== null) { queryArgs[key] = value; } } const regex = /\/:([^/]*)/; let url = route.url; let matches = regex.exec(url); while (matches) { const urlParam = request[matches[1]]; // check if an url param is missing (eg: "/:index/_create) if (!urlParam) { const error = new Error(`Missing URL param "${matches[1]}" in "${matches.input}"`); this.emit(payload.requestId, { error, status: 400 }); return; } url = url.replace(regex, `/${encodeURIComponent(request[matches[1]])}`); delete queryArgs[matches[1]]; matches = regex.exec(url); } // inject queryString arguments: const queryString = []; for (const key of Object.keys(queryArgs)) { let value = queryArgs[key]; const encodedKey = encodeURIComponent(key); if (Array.isArray(value)) { queryString.push(`${encodedKey}=${encodeURIComponent(value.join())}`); } else if (typeof value === "boolean") { // In Kuzzle, an optional boolean option is set to true if present in // the querystring, and false if absent. // As there is no boolean type in querystrings, encoding a boolean // option "foo=false" in it will make Kuzzle consider it as truthy. if (value === true) { queryString.push(encodedKey); } } else { value = typeof value === "object" ? JSON.stringify(value) : value; queryString.push(`${encodedKey}=${encodeURIComponent(value)}`); } } if (queryString.length > 0) { url += "?" + queryString.join("&"); } return { method, path: url, payload, }; } /** * Sends a payload to the connected server * * @param {Object} data * @returns {Promise<any>} */ send(request, options = {}) { const formattedRequest = this.formatRequest(request, options); if (formattedRequest) { this._sendHttpRequest(formattedRequest) .then((response) => this.emit(formattedRequest.payload.requestId, response)) .catch((error) => this.emit(formattedRequest.payload.requestId, { error })); } } _sendHttpRequest({ method, path, payload, }) { if (typeof XMLHttpRequest === "undefined") { // NodeJS implementation, using http.request: // eslint-disable-next-line @typescript-eslint/no-var-requires const httpClient = require("min-req-promise"); if (path[0] !== "/") { path = `/${path}`; } const url = `${this.protocol}://${this.host}:${this.port}${path}`; const headers = (payload && payload.headers) || {}; headers["Content-Length"] = Buffer.byteLength((payload && payload.body) || ""); return httpClient .request(url, method, { body: payload && payload.body, headers: headers, timeout: this._timeout, }) .then((response) => { if (response.statusCode === 431) { throw new Error("Request query string is too large. Try to use the method with the POST verb instead."); } const contentType = response.headers["content-type"]; if (!contentType || !contentType.includes("application/json")) { return response.body; } return JSON.parse(response.body); }); } // Browser implementation, using XMLHttpRequest: return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(), url = `${this.protocol}://${this.host}:${this.port}${path}`; xhr.timeout = this._timeout; xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 0) { reject(new Error("Cannot connect to host. Is the host online?")); } }; xhr.open(method, url); // Authorize the reception of cookies xhr.withCredentials = this.cookieSupport; for (const [header, value] of Object.entries((payload && payload.headers) || {})) { xhr.setRequestHeader(header, value); } xhr.onload = () => { try { const contentType = xhr.getResponseHeader("Content-Type"); if (!contentType || !contentType.includes("application/json")) { resolve(xhr.responseText); return; } const json = JSON.parse(xhr.responseText); resolve(json); } catch (err) { reject(err); } }; xhr.send(payload && payload.body); }); } _constructRoutes(publicApi) { const apiRoutes = Object.keys(publicApi) .map((key) => [key, publicApi[key]]) .reduce((routes, [controller, definition]) => { routes[controller] = {}; for (const action of Object.keys(definition)) { const { http } = definition[action]; if (http && http.length === 1) { routes[controller][action] = http[0]; } else if (http && http.length > 1) { // We need this ugly fix because the document:search route can also // be accessed in GET with this url: "/:index/:collection" // But to send a query, we need to pass it in the body so we need POST // so we can change the verb but then POST on "/:index/:collection" // is the collection:update method (document:search is "/:index/:collection/_search") if (controller === "document" && action === "search") { routes[controller][action] = getPostRoute(http); } else { routes[controller][action] = getCorrectRoute(http); } } } return routes; }, {}); for (const controller of Object.keys(this._customRoutes)) { apiRoutes[controller] = this._customRoutes[controller]; } return apiRoutes; } _warn(message) { console.warn(message); // eslint-disable-line no-console } } exports.default = HttpProtocol; function getPostRoute(routes) { return routes[0].verb === "POST" ? routes[0] : routes[1]; } function getCorrectRoute(routes) { let shortestRoute = routes[0]; let getRoute = routes[0]; let minLength = routes[0].url.length; let sameLength = true; for (const route of routes) { if (route.url.length !== minLength) { sameLength = false; } if (route.url.length < minLength) { shortestRoute = route; minLength = route.url.length; } if (route.verb === "GET") { getRoute = route; } } // with same URL size, we prefer the GET route // with differents URL sizes, we keep the shortest because URL params // will be in the query string return sameLength ? getRoute : shortestRoute; } //# sourceMappingURL=Http.js.map