server/lib/middleware/MiddlewareUtil.js

import parseurl from "parseurl";
import mime from "mime-types";
import {
	createReaderCollection,
	createReaderCollectionPrioritized,
	createResource,
	createFilterReader,
	createLinkReader,
	createFlatReader
} from "@ui5/fs/resourceFactory";

/**
 * Convenience functions for UI5 Server middleware.
 * An instance of this class is passed to every standard UI5 Server middleware.
 * Custom middleware that define a specification version >= 2.0 will also receive an instance
 * of this class as part of the parameters of their create-middleware function.
 *
 * The set of functions that can be accessed by a custom middleware depends on the specification
 * version defined for the extension.
 *
 * @public
 * @class
 * @alias @ui5/server/middleware/MiddlewareUtil
 * @hideconstructor
 */
class MiddlewareUtil {
	/**
	 *
	 * @param {object} parameters
	 * @param {@ui5/project/graph/ProjectGraph} parameters.graph Relevant ProjectGraph
	 * @param {@ui5/project/specifications/Project} parameters.project Project that is being served
	 * @public
	 */
	constructor({graph, project}) {
		if (!graph) {
			throw new Error(`Missing parameter "graph"`);
		}
		if (!project) {
			throw new Error(`Missing parameter "project"`);
		}
		this._graph = graph;
		this._project = project;
	}

	/**
	 * Returns the [pathname]{@link https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname}
	 * of a given request. Any escape sequences will be decoded.
	 * </br></br>
	 * This method is only available to custom middleware extensions defining
	 * <b>Specification Version 2.0 and above</b>.
	 *
	 * @param {object} req Request object
	 * @returns {string} [Pathname]{@link https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname}
	 * of the given request
	 * @public
	 */
	getPathname(req) {
		let {pathname} = parseurl(req);
		pathname = decodeURIComponent(pathname);
		return pathname;
	}

	/**
	 * MIME Info
	 *
	 * @example
	 * const mimeInfo = {
	 * 	"type": "text/html",
	 * 	"charset": "utf-8",
	 * 	"contentType": "text/html; charset=utf-8"
	 * };
	 *
	 * @public
	 * @typedef {object} MimeInfo
	 * @property {string} type Detected content-type for the given resource path
	 * @property {string} charset Default charset for the detected content-type
	 * @property {string} contentType Calculated content-type header value
	 * @memberof @ui5/server/middleware/MiddlewareUtil
	 */
	/**
	 * Returns MIME information derived from a given resource path.
	 * </br></br>
	 * This method is only available to custom middleware extensions defining
	 * <b>Specification Version 2.0 and above</b>.
	 *
	 * @param {object} resourcePath
	 * @returns {@ui5/server/middleware/MiddlewareUtil.MimeInfo}
	 * @public
	 */
	getMimeInfo(resourcePath) {
		const type = mime.lookup(resourcePath) || "application/octet-stream";
		const charset = mime.charset(type);
		return {
			type,
			charset,
			contentType: type + (charset ? "; charset=" + charset : "")
		};
	}
	/**
	 * Specification Version-dependent [Project]{@link @ui5/project/specifications/Project} interface.
	 * For details on individual functions, see [Project]{@link @ui5/project/specifications/Project}
	 *
	 * @public
	 * @typedef {object} @ui5/server/middleware/MiddlewareUtil~ProjectInterface
	 * @property {Function} getType Get the project type
	 * @property {Function} getName Get the project name
	 * @property {Function} getVersion Get the project version
	 * @property {Function} getNamespace Get the project namespace
	 * @property {Function} getRootReader Get the project rootReader
	 * @property {Function} getReader Get the project reader, defaulting to "runtime" style instead of "buildtime"
	 * @property {Function} getRootPath Get the local File System path of the project's root directory
	 * @property {Function} getSourcePath Get the local File System path of the project's source directory
	 * @property {Function} getCustomConfiguration Get the project Custom Configuration
	 * @property {Function} isFrameworkProject Check whether the project is a UI5-Framework project
	 * @property {Function} getFrameworkName Get the project's framework name configuration
	 * @property {Function} getFrameworkVersion Get the project's framework version configuration
	 * @property {Function} getFrameworkDependencies Get the project's framework dependencies configuration
	 */

	/**
	 * Retrieve a single project from the dependency graph
	 *
	 * </br></br>
	 * This method is only available to custom server middleware extensions defining
	 * <b>Specification Version 3.0 and above</b>.
	 *
	 * @param {string|@ui5/fs/Resource} [projectNameOrResource]
	 * Name of the project to retrieve or a Resource instance to retrieve the associated project for.
	 * Defaults to the name of the current root project
	 * @returns {@ui5/server/middleware/MiddlewareUtil~ProjectInterface|undefined}
	 * Specification Version-dependent interface to the Project instance or <code>undefined</code>
	 * if the project name is unknown or the provided resource is not associated with any project.
	 * @public
	 */
	getProject(projectNameOrResource) {
		if (projectNameOrResource) {
			if (typeof projectNameOrResource === "string" || projectNameOrResource instanceof String) {
				// A project name has been provided
				return this._graph.getProject(projectNameOrResource);
			} else {
				// A Resource instance has been provided
				return projectNameOrResource.getProject();
			}
		}
		// No parameter has been provided, default to the root project
		return this._project;
	}

	/**
	 * Retrieve a list of direct dependencies of a given project from the dependency graph.
	 * Note that this list does not include transitive dependencies.
	 *
	 * </br></br>
	 * This method is only available to custom server middleware extensions defining
	 * <b>Specification Version 3.0 and above</b>.
	 *
	 * @param {string} [projectName] Name of the project to retrieve.
	 * Defaults to the name of the current root project
	 * @returns {string[]} Names of all direct dependencies
	 * @throws {Error} If the requested project is unknown to the graph
	 * @public
	 */
	getDependencies(projectName) {
		return this._graph.getDependencies(projectName || this._project.getName());
	}

	/**
	 * Specification Version-dependent set of [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory}
	 * functions provided to middleware.
	 * For details on individual functions, see [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory}
	 *
	 * @public
	 * @typedef {object} @ui5/server/middleware/MiddlewareUtil~resourceFactory
	 * @property {Function} createResource Creates a [Resource]{@link @ui5/fs/Resource}.
	 * 	Accepts the same parameters as the [Resource]{@link @ui5/fs/Resource} constructor.
	 * @property {Function} createReaderCollection Creates a reader collection:
	 *	[ReaderCollection]{@link @ui5/fs/ReaderCollection}
	 * @property {Function} createReaderCollectionPrioritized Creates a prioritized reader collection:
	 *	[ReaderCollectionPrioritized]{@link @ui5/fs/ReaderCollectionPrioritized}
	 * @property {Function} createFilterReader
	 * 	Create a [Filter-Reader]{@link @ui5/fs/readers/Filter} with the given reader.
	 * @property {Function} createLinkReader
	 * 	Create a [Link-Reader]{@link @ui5/fs/readers/Filter} with the given reader.
	 * @property {Function} createFlatReader Create a [Link-Reader]{@link @ui5/fs/readers/Link}
	 * where all requests are prefixed with <code>/resources/<namespace></code>.
	 */

	/**
	 * Provides limited access to [@ui5/fs/resourceFactory]{@link @ui5/fs/resourceFactory} functions
	 *
	 * </br></br>
	 * This attribute is only available to custom server middleware extensions defining
	 * <b>Specification Version 3.0 and above</b>.
	 *
	 * @type {@ui5/server/middleware/MiddlewareUtil~resourceFactory}
	 * @public
	 */
	resourceFactory = {
		createResource,
		createReaderCollection,
		createReaderCollectionPrioritized,
		createFilterReader,
		createLinkReader,
		createFlatReader,
	};

	/**
	 * Get an interface to an instance of this class that only provides those functions
	 * that are supported by the given custom middleware extension specification version.
	 *
	 * @param {@ui5/project/specifications/SpecificationVersion} specVersion
	 * SpecVersionComparator instance of the custom server middleware
	 * @returns {object} An object with bound instance methods supported by the given specification version
	 */
	getInterface(specVersion) {
		if (specVersion.lt("2.0")) {
			// Custom middleware defining specVersion <2.0 does not have access to any MiddlewareUtil API
			return undefined;
		}

		const baseInterface = {};
		bindFunctions(this, baseInterface, [
			"getPathname", "getMimeInfo"
		]);

		if (specVersion.gte("3.0")) {
			// getProject function, returning an interfaced project instance
			baseInterface.getProject = (projectName) => {
				const project = this.getProject(projectName);
				const baseProjectInterface = {};
				bindFunctions(project, baseProjectInterface, [
					"getType", "getName", "getVersion", "getNamespace",
					"getRootReader", "getRootPath", "getSourcePath",
					"getCustomConfiguration", "isFrameworkProject", "getFrameworkName",
					"getFrameworkVersion", "getFrameworkDependencies"
				]);
				// Project#getReader defaults to style "buildtime". However ui5-server uses
				// style "runtime". The main difference is that for some project types (like applications)
				// the /resources/<namespace> path prefix is omitted for "runtime". Also, no builder resource-
				// exclude configuration is applied.
				// Therefore default to style "runtime" here so that custom middleware will commonly work with
				// the same paths as ui5-server and no unexpected builder-excludes.
				baseProjectInterface.getReader = function(options = {style: "runtime"}) {
					return project.getReader(options);
				};
				return baseProjectInterface;
			};
			// getDependencies function, returning an array of project names
			baseInterface.getDependencies = (projectName) => {
				return this.getDependencies(projectName);
			};

			baseInterface.resourceFactory = Object.create(null);
			[
				// Once new functions get added, extract this array into a variable
				// and enhance based on spec version once new functions get added
				"createResource", "createReaderCollection", "createReaderCollectionPrioritized",
				"createFilterReader", "createLinkReader", "createFlatReader",
			].forEach((factoryFunction) => {
				baseInterface.resourceFactory[factoryFunction] = this.resourceFactory[factoryFunction];
			});
		}
		return baseInterface;
	}
}

function bindFunctions(sourceObject, targetObject, funcNames) {
	funcNames.forEach((funcName) => {
		targetObject[funcName] = sourceObject[funcName].bind(sourceObject);
	});
}

export default MiddlewareUtil;