UNPKG

@webda/core

Version:

Expose API with Lambda

728 lines 20.2 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import acceptLanguage from "accept-language"; import { EventEmitter } from "events"; import sanitize from "sanitize-html"; import { Readable } from "stream"; import { WritableStreamBuffer } from "stream-buffers"; import { NotEnumerable } from "../models/coremodel.js"; import { Session } from "../utils/session.js"; import { JSONUtils } from "./serializers.js"; /** * @category CoreFeatures */ class Cookie { } /** * OperationContext is used when call to an operation * * @param T type of input for this context * @param U type of output for this context */ export class OperationContext extends EventEmitter { /** * @ignore * Used by Webda framework to set the body, session and output stream if known */ constructor(webda, stream = undefined) { super(); this.extensions = {}; this._webda = webda; this._promises = []; this._body = undefined; this._stream = stream; if (stream === undefined) { this.createStream(); } } /** * Get an extension of the context * @param name of the extension * @returns extension object */ getExtension(name) { return this.extensions[name]; } /** * For easier compatibility with WebContext * On OperationContext this call is simply ignored */ setHeader(_name, _value) { // Do nothing } /** * For easier compatibility with WebContext * On OperationContext this call is simply ignored */ writeHead(_code, _headers) { // Do nothing } /** * * @param name to add * @param extension object to store */ setExtension(name, extension) { this.extensions[name] = extension; return this; } /** * Return the webda */ getWebda() { return this._webda; } /** * Register a promise with the context * @param promise */ addAsyncRequest(promise) { this._promises.push(promise); } /** * Get output as string, if a OutputStream is provided it will returned null * @returns */ getOutput() { if (this._stream instanceof WritableStreamBuffer && this._stream.size()) { return this._stream.getContents().toString(); } return this._body; } /** * Get current http context */ getHttpContext() { return this.getExtension("http"); } /** * Ensure the whole execution is finished */ async end() { this.emit("end"); await Promise.all(this._promises); this.emit("close"); } async getInput(sanitizedOptions = { allowedTags: [], allowedAttributes: {} }) { if (this._sanitized && !sanitizedOptions.raw) { return this._sanitized; } let recursiveSanitize = (obj, options = undefined, path = "") => { if (typeof obj === "string") { return sanitize(obj, options); } if (obj !== null && typeof obj === "object") { Object.keys(obj).forEach(key => { obj[key] = Array.isArray(sanitizedOptions.raw) && sanitizedOptions.raw.includes(path + key) ? obj[key] : recursiveSanitize(obj[key], options, path + key + "."); }); } return obj; }; try { let data = await this.getRawInputAsString(this.getWebda().getGlobalParams().requestLimit, this.getWebda().getGlobalParams().requestTimeout); if (sanitizedOptions.raw === true) { return JSON.parse(data || sanitizedOptions.defaultValue); } if (!data || data.length === 0) { this._sanitized = sanitizedOptions.defaultValue; return this._sanitized; } this._sanitized = recursiveSanitize(JSON.parse(data), sanitizedOptions); } catch (err) { this.log("ERROR", err, `Body: '${await this.getRawInputAsString()}'`); this._sanitized = sanitizedOptions.defaultValue; } return this._sanitized; } /** * By default empty * @returns */ async getRawInputAsString(limit = 1024 * 1024 * 10, timeout = 60000, encoding) { return (await this.getRawInput(limit, timeout)).toString(encoding); } /** * @override */ async getRawInput(_limit = 1024 * 1024 * 10, _timeout = 60000) { return Buffer.from(""); } /** * @override */ getRawStream() { return undefined; } /** * Get the HTTP stream to output raw data * @returns {*} */ getOutputStream() { return this._stream; } /** * Get linked session * @returns */ getSession() { return this.session; } /** * Remove sanitized body */ reinit() { this._sanitized = undefined; if (!this._stream || this._stream instanceof WritableStreamBuffer) { this.createStream(); } } /** * Create a buffer stream */ createStream() { this._stream = new WritableStreamBuffer({ initialSize: 100 * 1024, incrementAmount: 100 * 1024 }); } /** * Proxy for simplification * @param level * @param args */ log(level, ...args) { this._webda.log(level, ...args); } /** * Create a new session * @returns */ async newSession() { this.session = await this._webda.getService("SessionManager").newSession(this); return this.session; } /** * Remove everything that was about to be sent */ resetResponse() { this._body = undefined; if (this._stream instanceof WritableStreamBuffer) { this.createStream(); } } /** * Write data to the client * * @param output If it is an object it will be serialized with toPublicJSON, if it is a String it will be appended to the result, if it is a buffer it will replace the result * @param ...args any arguments to pass to the toPublicJSON method */ write(output, _encoding, _cb) { if (!output) { return false; } if (typeof output === "object" && !(output instanceof Buffer)) { this._body = JSONUtils.stringify(output, undefined, 0, true); } else if (typeof output == "string") { if (this._body == undefined) { this._body = ""; } this._body += output; } else { this._body = output.toString(); } return true; } async init() { return this; } /** * Get the current user from session */ async getCurrentUser(refresh = false) { if (!this.getCurrentUserId()) { return undefined; } // Caching the answer if (!this.user || refresh) { this.user = await this._webda.getApplication().getModel("User").ref(this.getCurrentUserId()).get(this); } return this.user; } /** * Get the current user id from session */ getCurrentUserId() { return this.session?.userId; } /** * Global context is the default Context * * Whenever a request is internal to the system * or not linked to a user request * @returns */ isGlobal() { return false; } } __decorate([ NotEnumerable ], OperationContext.prototype, "_webda", void 0); __decorate([ NotEnumerable ], OperationContext.prototype, "_promises", void 0); __decorate([ NotEnumerable ], OperationContext.prototype, "_stream", void 0); export class GlobalContext extends OperationContext { constructor(webda) { super(webda); this.session = new Session(); this.session.login("system", "system"); // Disable logout this.session.logout = () => { }; } /** * @override */ isGlobal() { return true; } } /** * Simple Operation Context with custom input */ export class SimpleOperationContext extends OperationContext { constructor(webda) { super(webda); } /** * Create another context from an existing one * @param context * @returns */ static async fromContext(context) { const ctx = new SimpleOperationContext(context.getWebda()); ctx.setSession(context.getSession()); ctx.setInput(Buffer.from(JSONUtils.stringify(await context.getInput()))); return ctx; } /** * Set the input */ setInput(input) { this.input = input; return this; } /** * Set the session * @param session * @returns */ setSession(session) { this.session = session; return this; } /** * @override */ async getRawInput(limit = 1024 * 1024 * 10, _timeout = 60000) { return this.input.slice(0, limit); } } /** * This represent in fact a WebContext * In 3.0 an abstract version of Context will replace this (closer to OperationContext) * @category CoreFeatures * */ export class WebContext extends OperationContext { /** * Set current http context * @param httpContext current http context */ setHttpContext(httpContext) { this.extensions["http"] = httpContext; this.reinit(); } /** * @override */ async getRawInputAsString(limit = 1024 * 1024 * 10, timeout = 60000, encoding) { return this.getHttpContext().getRawBodyAsString(limit, timeout, encoding); } /** * @override */ async getRawInput(limit = 1024 * 1024 * 10, timeout = 60000) { return this.getHttpContext().getRawBody(limit, timeout); } /** * @override */ getRawStream() { return this.getHttpContext().getRawStream(); } /** * Get output headers */ getResponseHeaders() { return this._outputHeaders; } getRequestParameters() { return this.parameters; } parameter(name) { return this.getParameters()[name]; } getParameters() { return this.parameters; } processParameters() { this.parameters = Object.assign({}, this._serviceParams); this.parameters = Object.assign(this.parameters, this._pathParams); } getServiceParameters() { return this._serviceParams; } getPathParameters() { return this._pathParams; } setServiceParameters(params) { this._serviceParams = params; this.processParameters(); } setPathParameters(params) { this._pathParams = params; this.processParameters(); } /** * Remove everything that was about to be sent */ resetResponse() { this._outputHeaders = {}; super.resetResponse(); } /** * Write data to the client * * @param output If it is an object it will be serializeb with toPublicJSON, if it is a String it will be appended to the result, if it is a buffer it will replace the result * @param ...args any arguments to pass to the toPublicJSON method */ // @ts-ignore write(output, encoding, cb) { if (this.statusCode === 204) { this.statusCode = 200; } if (typeof output === "object" && !(output instanceof Buffer) && !this.hasFlushedHeaders()) { this.setHeader("Content-type", "application/json"); } return super.write(output, encoding, cb); } /** * Set a header value * * @param {String} header name * @param {String} value */ setHeader(header, value) { if (this.headersFlushed) { throw new Error("Headers have been sent already"); } if (value) { this._outputHeaders[header] = value; } else if (this._outputHeaders[header]) { delete this._outputHeaders[header]; } } /** * Write the http return code and some headers * Those headers are not flushed yet so can still be overwritten * * @param {Number} statusCode to return to the client * @param {Object} headers to add to the response */ writeHead(statusCode, headers = undefined) { this._outputHeaders = { ...this._outputHeaders, ...headers }; // Ensure undefined values are removed Object.keys(this._outputHeaders) .filter(h => this._outputHeaders[h] === undefined) .forEach(h => delete this._outputHeaders[h]); if (statusCode !== undefined) { this.statusCode = statusCode; } return this; } /** * * @returns */ getResponseCode() { return this.statusCode || 200; } /** * Redirect to another url * @param url */ redirect(url) { this.writeHead(302, { Location: url }); } /** * For compatibility reason */ cookie(param, value, options = undefined) { /** @ignore */ if (this._cookie === undefined) { this._cookie = new Map(); } this._cookie[param] = { name: param, value, options }; } getResponseCookies() { return this._cookie; } isEnded() { return this._ended; } /****************************** * * Express Compatibiliy method * ******************************/ /** * Express response allow statusCode to be defined this way * @param code to return */ status(code) { this.statusCode = code; return this; } /** * Express response allow answer to be sent this way * @param code to return */ json(obj) { this.write(obj); return this; } /** * Return the response size * @returns */ getResponseSize() { return this._body ? Buffer.byteLength(this._body, "utf8") : undefined; } /** * Flush the request * * @emits 'finish' event * @throws Error if the request was already ended */ async end() { /** @ignore */ if (this._ended) { return this._ended; } this._ended = (async () => { this.emit("end"); if (this.getExtension("http")) { await this._webda.getService("SessionManager").save(this, this.session); } await Promise.all(this._promises); if (this._stream instanceof WritableStreamBuffer && this._stream.size()) { this._body = this._stream.getContents().toString(); this.statusCode = this.statusCode < 300 ? 200 : this.statusCode; } if (!this.headersFlushed) { this._webda.flushHeaders(this); } this._webda.flush(this); this.emit("close"); })(); return this._ended; } /** * Alias to keep compatibility with WebContext * @param sanitizedOptions * @returns */ async getRequestBody(sanitizedOptions = { allowedTags: [], allowedAttributes: {} }) { return this.getInput(sanitizedOptions); } /** * Get request body * @returns */ getResponseBody() { if (!this._body && this._stream instanceof WritableStreamBuffer) { return this._stream.getContents(); } return this._body; } /** * Retrieve a http.IncomingMessage valid from Context * * Need more testing * @returns */ getRequest() { const stream = Readable.from([JSON.stringify(this.getRequestBody())]); return { httpVersionMajor: 1, httpVersionMinor: 0, headers: this.headers, httpVersion: "1.0", method: this.getHttpContext().getMethod(), rawHeaders: [], // TODO Regenerate headers based on the map rawTrailers: [], setTimeout: (msec, callback) => { setTimeout(callback, msec); }, socket: undefined, statusCode: 200, trailers: {}, url: this.getHttpContext().getUrl(), connection: undefined, ...stream }; } /** * Get a service from webda * * @see Webda * @param {String} name of the service */ getService(name) { return this._webda.getService(name); } /** * Get the HTTP stream to output raw data * @returns {*} */ getStream() { return this.getOutputStream(); } /** * Get the current user id from session */ getCurrentUserId() { if (this.session) { return this.session.userId; } return undefined; } /** * Return the service handling the request */ getExecutor() { return this._executor; } /** * Execute the target route */ async execute() { return this._route._method(this); } /** * Get the request locale if found */ getLocale() { let locales = this._webda.getLocales(); acceptLanguage.languages(locales); let header = this.getHttpContext().getUniqueHeader("accept-language"); if (header) { return acceptLanguage.get(header); } return locales[0]; } /** * @ignore * Used by Webda framework to set the current route */ setRoute(route) { this._route = route; this.parameters = { ...route.params, ...this.parameters }; } getRoute() { return this._route; } /** * @param executor {object} Set the current executor for this context */ setExecutor(executor) { this._executor = executor; } /** * @ignore * Used for compatibility with express module */ logIn() { // Empty for compatibility } /** * Return true if Headers got flushed already * @returns */ hasFlushedHeaders() { return this.headersFlushed; } /** * Set flushed header status * @param status */ setFlushedHeaders(status = true) { this.headersFlushed = status; } /** * @ignore * Used by Webda framework to set the body, session and output stream if known */ constructor(webda, httpContext, stream = undefined) { super(webda, stream); this._ended = undefined; this.parameters = undefined; this._pathParams = {}; this._serviceParams = {}; this.setHttpContext(httpContext); this._outputHeaders = {}; this.headersFlushed = false; this.statusCode = 204; this.parameters = {}; this.headers = new Map(); this.processParameters(); } async init(force = false) { if (this._init && !force) { return this._init; } this._stream.on("pipe", () => { this._webda.flushHeaders(this); this.headersFlushed = true; }); if (this.getExtension("http")) { this.session = (await this._webda.getService("SessionManager").load(this)).getProxy(); } this._init = super.init(); return this._init; } emitError(err) { this.emit("error", err); } } //# sourceMappingURL=context.js.map