<commit_before>/**
 * Kettle Requests
 *
 * Copyright 2012-2013 OCAD University
 *
 * Licensed under the New BSD license. You may not use this file except in
 * compliance with this License.
 *
 * You may obtain a copy of the License at
 * https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
 */

"use strict";

var fluid = require("infusion"),
    kettle = fluid.registerNamespace("kettle");

fluid.defaults("kettle.requests", {
    gradeNames: ["fluid.component"],
    events: {
        createRequest: null // fired by our handler once express determines that route + verb is a match
    },
    dynamicComponents: {
        request: {
            createOnEvent: "createRequest",
            type: "{arguments}.0.type",
            options: "{arguments}.0.options"
        }
    }
});

// The member name of the currently active request as held in Infusion's "resolve root" component
kettle.requestMemberName = fluid.typeNameToMemberName("kettle.request");

kettle.getCurrentRequest = function () {
    return fluid.resolveRootComponent[kettle.requestMemberName];
};

kettle.markActiveRequest = function (request) {
    var parent = fluid.resolveRootComponent,
        memberName = kettle.requestMemberName,
        instantiator = fluid.globalInstantiator,
        innerRequest = kettle.getCurrentRequest();
    if (request && innerRequest && innerRequest !== request) {
        fluid.fail("Error marking thread to request " + request.id + " - this thread is already marked to request " + innerRequest.id + " . Make sure to invoke this request asynchronously.");
    }
    if (request) {
        if (innerRequest !== request) {
            if (fluid.isDestroyed(request)) {
                fluid.fail("Error marking thread to request " + request.id + " which has already been destroyed");
            }
            instantiator.recordKnownComponent(parent, request, memberName, false);
        }
    } else {
        if (parent[memberName]) { // unmarked automatically if destroyed
            instantiator.clearComponent(parent, memberName);
        }
    }
    return innerRequest;
};

// Returns a function wrapping the supplied callback with the supplied request - the
// callback will be executed in an environment where the request has been restored
kettle.withRequest = function (request, callback) {
    return function wrappedCallback() {
        // Avoid double-wrapping the same stack, just as a courtesy for debugging and efficiency
        // To be compatible to both when.js < 2.0.0 and when >= 2.0.0 we must be prepared for superfluous wrapping sometimes
        // Although in theory this could be removed now that when.js is gone, there are still use cases where the user may supply, e.g.
        // synchronously resolving promises.
        if (request && fluid.isDestroyed(request)) {
            fluid.log("Failing to resume callback for request " + request.id + " which has already concluded");
            return;
        }
        var innerRequest = kettle.markActiveRequest(request);
        if (innerRequest) {
            return callback.apply(null, arguments);
        } else {
            try {
                return callback.apply(null, arguments);
            } catch (err) { // ensure we handle this before we lose marking
                fluid.onUncaughtException.fire(err);
            } finally {
                kettle.markActiveRequest(null);
            }
        }
    };
};

// Every genuinely asynchronous callback propagated by a Kettle app must be wrapped by this
// function - in order to propagate the "marker" which identifies the current request object.
kettle.wrapCallback = function (callback) {
    var request = kettle.getCurrentRequest();
    return kettle.withRequest(request, callback);
};

fluid.defaults("kettle.request", {
    gradeNames: ["fluid.component"],
    mergePolicy: {
        requestMiddleware: "noexpand"
    },
    invokers: {
        handleRequest: "fluid.notImplemented",
        handleFullRequest: "fluid.notImplemented"
    },
    events: {
        onHandle: null,  // Fired in order to execute its listener, handleRequest, which invokes the user's handler
        onSuccess: null, // A convenient proxy for the main request handler to report disposition (equivalent to handlerPromise)
        onError: null,  // A convenient proxy for the main request handler to report disposition (equivalent to handlerPromise)
        onRequestEnd: null,
        onRequestSuccess: null, // Overall disposition of the entire request - handler actually sends response
        onRequestError: null // Overall disposition of the entire request - handler actually sends response
    },
    // sourced from dynamic args
    req: "{arguments}.1",
    res: "{arguments}.2",
    next: "{arguments}.3",
    members: {
        req: "{that}.options.req", // TODO: can't supply these direct from args because of framework members merging bug - get array instead
        res: "{that}.options.res",
        next: "{that}.options.next",
        /* Construct a "promisified" proxy for the handling of this request. It will
         * have handlers registered which forward to this request's success and error events.
         * The promise may be rejected or resolved by the user to represent that disposition
         * for the overall request */
        handlerPromise: "@expand:fluid.promise()"
    },
    listeners: {
        "onCreate.activate": "kettle.request.activate",
        "onHandle.handleRequest": {
            funcName: "kettle.request.handleRequest",
            args: "{that}"
        },
        "onSuccess.forward": "kettle.request.forwardPromise(resolve, {that}.handlerPromise, {arguments}.0)",
        "onError.forward": "kettle.request.forwardPromise(reject, {that}.handlerPromise, {arguments}.0)"
    }
});

// A handler which reports a 404 if the request has not already been handled (e.g. by some middleware, e.g. static)
kettle.request.notFoundHandler = function (request) {
    /* istanbul ignore else - much middleware is naughty and does not call "next" if it thinks it has fully handled the request */
    if (!request.handlerPromise.disposition) {
        request.handlerPromise.reject({statusCode: 404, message: "Cannot " + request.req.method + " " + request.req.originalUrl});
    }
};

kettle.request.forwardPromise = function (method, promise, val) {
    if (!promise.disposition) {
        promise[method](val);
    } else {
        // Don't use fluid.fail here to avoid infinite triggering of errors
        fluid.log("Error in forwarding result ", val, " to promise " + method + ": promise has already received " + promise.disposition);
    }
};

kettle.request.activate = function (that) {
    that.req.fluidRequest = that;
    kettle.markActiveRequest(that);
};

kettle.request.clear = function () {
    kettle.markActiveRequest(null);
};

kettle.request.handleRequest = function (that) {
    try {
        that.handleRequest(that);
    } catch (err) {
        if (!that.handlerPromise.disposition) {
            that.handlerPromise.reject({message: err.message, stack: err.stack});
        }
    } finally {
        kettle.markActiveRequest(null);
    }
};

// A function representing the "handler executing task" of the request's sequence

kettle.request.handleRequestTask = function (request) {
    if (!request.res.finished) { // don't handle if some middleware has already sent a full response - makes us deterministic on node 4
        request.events.onHandle.fire(request); // our actual request handler triggerer
    }
    return request.handlerPromise;
};

fluid.defaults("kettle.request.mismatch", {
    requestMiddleware: {
        mismatch: {
            middleware: "{middlewareHolder}.mismatch",
            priority: "first"
        }
    }
});

fluid.defaults("kettle.request.http", {
    gradeNames: ["kettle.request"],
    invokers: {
        handleFullRequest: "kettle.request.http.handleFullRequest"
    },
    listeners: {
        "onCreate.ensureResponseDisposes": {
            funcName: "kettle.request.http.ensureResponseDisposes",
            priority: "before:handleRequest"
        },
        "onRequestError.handle": {
            funcName: "kettle.request.http.errorHandler",
            args: ["{that}.res", "{arguments}.0"]
        },
        "onRequestSuccess.handle": {
            funcName: "kettle.request.http.successHandler",
            args: ["{that}", "{arguments}.0"]
        }
    }
});

fluid.defaults("kettle.request.http.mismatch", {
    gradeNames: ["kettle.request.http", "kettle.request.mismatch"],
    invokers: {
        handleRequest: "fluid.identity"
    }
});

kettle.request.http.handleFullRequest = function (request, fullRequestPromise, next) {
    fullRequestPromise.then(function (response) {
        request.events.onRequestSuccess.fire(response);
        next();
    }, function (err) {
        request.events.onRequestError.fire(err);
        // A variant implementation could decide to call next(err) if we were interested in invoking express' upstream handler
        next();
    });
};

/**
 * Send an error payload to the client if the request ends in error
 * @param  {Object} res an Express response object.
 * @param  {Object} error an error payload. Should include fields statusCode holding a numeric HTTP status code, and message holding an error message.
 */
kettle.request.http.errorHandler = function (res, error) {
    var outError = fluid.extend(true, {
        isError: true,
        message: "Unknown error",
        statusCode: 500
    }, error);
    if (error.message) { // Error object's "message" property fails to clone regularly on node 4.x
        outError.message = error.message;
    }
    res.status(outError.statusCode).json(fluid.censorKeys(outError, ["statusCode"]));
    return outError;
};

/**
 * Send a successful payload to the client if the success event is fired.
 * @param  {Object} res an Express response object.
 * @param  {Object} response a success payload.
 */
kettle.request.http.successHandler = function (request, response) {
    if (request.req.method.toLowerCase() === "options") {
        request.res.status(200).end();
        return;
    }
    if (typeof(response) !== "string") {
        request.res.json(response);
    } else {
        request.res.status(200).send(response);
    }
};

/**
 * Ensure that the response is properly disposed of when finished.
 * @param {Object} that request object.
 */
kettle.request.http.ensureResponseDisposes = function (that) {
    // NOTE: This is here because any of these events can represent the
    // moment when the server is finished with the response.
    // TODO: We perhaps want to extend the possibility that further handlers may interpose
    // AFTER the main "handler" - in which case this timing point just becomes a prerequisite for
    // disposal rather than the direct trigger
    fluid.each(["close", "finish", "end", "error"], function addListener(event) {
        that.res.on(event, function eventListener() {
            // TODO: need to write a test case to validate possibility that this request may be destroyed
            if (!fluid.isDestroyed(that)) {
                that.events.onRequestEnd.fire();
                that.destroy();
            }
        });
    });
};
<commit_msg>GPII-1872: A minor change on KettleRequest.js so it can be used in-browser.<commit_after>/**
 * Kettle Requests
 *
 * Copyright 2012-2013 OCAD University
 *
 * Licensed under the New BSD license. You may not use this file except in
 * compliance with this License.
 *
 * You may obtain a copy of the License at
 * https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
 */

"use strict";

var fluid = fluid || require("infusion"),
    kettle = fluid.registerNamespace("kettle");

fluid.defaults("kettle.requests", {
    gradeNames: ["fluid.component"],
    events: {
        createRequest: null // fired by our handler once express determines that route + verb is a match
    },
    dynamicComponents: {
        request: {
            createOnEvent: "createRequest",
            type: "{arguments}.0.type",
            options: "{arguments}.0.options"
        }
    }
});

// The member name of the currently active request as held in Infusion's "resolve root" component
kettle.requestMemberName = fluid.typeNameToMemberName("kettle.request");

kettle.getCurrentRequest = function () {
    return fluid.resolveRootComponent[kettle.requestMemberName];
};

kettle.markActiveRequest = function (request) {
    var parent = fluid.resolveRootComponent,
        memberName = kettle.requestMemberName,
        instantiator = fluid.globalInstantiator,
        innerRequest = kettle.getCurrentRequest();
    if (request && innerRequest && innerRequest !== request) {
        fluid.fail("Error marking thread to request " + request.id + " - this thread is already marked to request " + innerRequest.id + " . Make sure to invoke this request asynchronously.");
    }
    if (request) {
        if (innerRequest !== request) {
            if (fluid.isDestroyed(request)) {
                fluid.fail("Error marking thread to request " + request.id + " which has already been destroyed");
            }
            instantiator.recordKnownComponent(parent, request, memberName, false);
        }
    } else {
        if (parent[memberName]) { // unmarked automatically if destroyed
            instantiator.clearComponent(parent, memberName);
        }
    }
    return innerRequest;
};

// Returns a function wrapping the supplied callback with the supplied request - the
// callback will be executed in an environment where the request has been restored
kettle.withRequest = function (request, callback) {
    return function wrappedCallback() {
        // Avoid double-wrapping the same stack, just as a courtesy for debugging and efficiency
        // To be compatible to both when.js < 2.0.0 and when >= 2.0.0 we must be prepared for superfluous wrapping sometimes
        // Although in theory this could be removed now that when.js is gone, there are still use cases where the user may supply, e.g.
        // synchronously resolving promises.
        if (request && fluid.isDestroyed(request)) {
            fluid.log("Failing to resume callback for request " + request.id + " which has already concluded");
            return;
        }
        var innerRequest = kettle.markActiveRequest(request);
        if (innerRequest) {
            return callback.apply(null, arguments);
        } else {
            try {
                return callback.apply(null, arguments);
            } catch (err) { // ensure we handle this before we lose marking
                fluid.onUncaughtException.fire(err);
            } finally {
                kettle.markActiveRequest(null);
            }
        }
    };
};

// Every genuinely asynchronous callback propagated by a Kettle app must be wrapped by this
// function - in order to propagate the "marker" which identifies the current request object.
kettle.wrapCallback = function (callback) {
    var request = kettle.getCurrentRequest();
    return kettle.withRequest(request, callback);
};

fluid.defaults("kettle.request", {
    gradeNames: ["fluid.component"],
    mergePolicy: {
        requestMiddleware: "noexpand"
    },
    invokers: {
        handleRequest: "fluid.notImplemented",
        handleFullRequest: "fluid.notImplemented"
    },
    events: {
        onHandle: null,  // Fired in order to execute its listener, handleRequest, which invokes the user's handler
        onSuccess: null, // A convenient proxy for the main request handler to report disposition (equivalent to handlerPromise)
        onError: null,  // A convenient proxy for the main request handler to report disposition (equivalent to handlerPromise)
        onRequestEnd: null,
        onRequestSuccess: null, // Overall disposition of the entire request - handler actually sends response
        onRequestError: null // Overall disposition of the entire request - handler actually sends response
    },
    // sourced from dynamic args
    req: "{arguments}.1",
    res: "{arguments}.2",
    next: "{arguments}.3",
    members: {
        req: "{that}.options.req", // TODO: can't supply these direct from args because of framework members merging bug - get array instead
        res: "{that}.options.res",
        next: "{that}.options.next",
        /* Construct a "promisified" proxy for the handling of this request. It will
         * have handlers registered which forward to this request's success and error events.
         * The promise may be rejected or resolved by the user to represent that disposition
         * for the overall request */
        handlerPromise: "@expand:fluid.promise()"
    },
    listeners: {
        "onCreate.activate": "kettle.request.activate",
        "onHandle.handleRequest": {
            funcName: "kettle.request.handleRequest",
            args: "{that}"
        },
        "onSuccess.forward": "kettle.request.forwardPromise(resolve, {that}.handlerPromise, {arguments}.0)",
        "onError.forward": "kettle.request.forwardPromise(reject, {that}.handlerPromise, {arguments}.0)"
    }
});

// A handler which reports a 404 if the request has not already been handled (e.g. by some middleware, e.g. static)
kettle.request.notFoundHandler = function (request) {
    /* istanbul ignore else - much middleware is naughty and does not call "next" if it thinks it has fully handled the request */
    if (!request.handlerPromise.disposition) {
        request.handlerPromise.reject({statusCode: 404, message: "Cannot " + request.req.method + " " + request.req.originalUrl});
    }
};

kettle.request.forwardPromise = function (method, promise, val) {
    if (!promise.disposition) {
        promise[method](val);
    } else {
        // Don't use fluid.fail here to avoid infinite triggering of errors
        fluid.log("Error in forwarding result ", val, " to promise " + method + ": promise has already received " + promise.disposition);
    }
};

kettle.request.activate = function (that) {
    that.req.fluidRequest = that;
    kettle.markActiveRequest(that);
};

kettle.request.clear = function () {
    kettle.markActiveRequest(null);
};

kettle.request.handleRequest = function (that) {
    try {
        that.handleRequest(that);
    } catch (err) {
        if (!that.handlerPromise.disposition) {
            that.handlerPromise.reject({message: err.message, stack: err.stack});
        }
    } finally {
        kettle.markActiveRequest(null);
    }
};

// A function representing the "handler executing task" of the request's sequence

kettle.request.handleRequestTask = function (request) {
    if (!request.res.finished) { // don't handle if some middleware has already sent a full response - makes us deterministic on node 4
        request.events.onHandle.fire(request); // our actual request handler triggerer
    }
    return request.handlerPromise;
};

fluid.defaults("kettle.request.mismatch", {
    requestMiddleware: {
        mismatch: {
            middleware: "{middlewareHolder}.mismatch",
            priority: "first"
        }
    }
});

fluid.defaults("kettle.request.http", {
    gradeNames: ["kettle.request"],
    invokers: {
        handleFullRequest: "kettle.request.http.handleFullRequest"
    },
    listeners: {
        "onCreate.ensureResponseDisposes": {
            funcName: "kettle.request.http.ensureResponseDisposes",
            priority: "before:handleRequest"
        },
        "onRequestError.handle": {
            funcName: "kettle.request.http.errorHandler",
            args: ["{that}.res", "{arguments}.0"]
        },
        "onRequestSuccess.handle": {
            funcName: "kettle.request.http.successHandler",
            args: ["{that}", "{arguments}.0"]
        }
    }
});

fluid.defaults("kettle.request.http.mismatch", {
    gradeNames: ["kettle.request.http", "kettle.request.mismatch"],
    invokers: {
        handleRequest: "fluid.identity"
    }
});

kettle.request.http.handleFullRequest = function (request, fullRequestPromise, next) {
    fullRequestPromise.then(function (response) {
        request.events.onRequestSuccess.fire(response);
        next();
    }, function (err) {
        request.events.onRequestError.fire(err);
        // A variant implementation could decide to call next(err) if we were interested in invoking express' upstream handler
        next();
    });
};

/**
 * Send an error payload to the client if the request ends in error
 * @param  {Object} res an Express response object.
 * @param  {Object} error an error payload. Should include fields statusCode holding a numeric HTTP status code, and message holding an error message.
 */
kettle.request.http.errorHandler = function (res, error) {
    var outError = fluid.extend(true, {
        isError: true,
        message: "Unknown error",
        statusCode: 500
    }, error);
    if (error.message) { // Error object's "message" property fails to clone regularly on node 4.x
        outError.message = error.message;
    }
    res.status(outError.statusCode).json(fluid.censorKeys(outError, ["statusCode"]));
    return outError;
};

/**
 * Send a successful payload to the client if the success event is fired.
 * @param  {Object} res an Express response object.
 * @param  {Object} response a success payload.
 */
kettle.request.http.successHandler = function (request, response) {
    if (request.req.method.toLowerCase() === "options") {
        request.res.status(200).end();
        return;
    }
    if (typeof(response) !== "string") {
        request.res.json(response);
    } else {
        request.res.status(200).send(response);
    }
};

/**
 * Ensure that the response is properly disposed of when finished.
 * @param {Object} that request object.
 */
kettle.request.http.ensureResponseDisposes = function (that) {
    // NOTE: This is here because any of these events can represent the
    // moment when the server is finished with the response.
    // TODO: We perhaps want to extend the possibility that further handlers may interpose
    // AFTER the main "handler" - in which case this timing point just becomes a prerequisite for
    // disposal rather than the direct trigger
    fluid.each(["close", "finish", "end", "error"], function addListener(event) {
        that.res.on(event, function eventListener() {
            // TODO: need to write a test case to validate possibility that this request may be destroyed
            if (!fluid.isDestroyed(that)) {
                that.events.onRequestEnd.fire();
                that.destroy();
            }
        });
    });
};
<|endoftext|>


<commit_before>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
"use strict";

const { Tapable, SyncHook, MultiHook } = require("tapable");
const asyncLib = require("neo-async");
const MultiWatching = require("./MultiWatching");
const MultiStats = require("./MultiStats");
const ConcurrentCompilationError = require("./ConcurrentCompilationError");

module.exports = class MultiCompiler extends Tapable {
	constructor(compilers) {
		super();
		this.hooks = {
			done: new SyncHook(["stats"]),
			invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
			run: new MultiHook(compilers.map(c => c.hooks.run)),
			watchClose: new SyncHook([]),
			watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun))
		};
		if (!Array.isArray(compilers)) {
			compilers = Object.keys(compilers).map(name => {
				compilers[name].name = name;
				return compilers[name];
			});
		}
		this.compilers = compilers;
		let doneCompilers = 0;
		let compilerStats = [];
		let index = 0;
		for (const compiler of this.compilers) {
			let compilerDone = false;
			const compilerIndex = index++;
			// eslint-disable-next-line no-loop-func
			compiler.hooks.done.tap("MultiCompiler", stats => {
				if (!compilerDone) {
					compilerDone = true;
					doneCompilers++;
				}
				compilerStats[compilerIndex] = stats;
				if (doneCompilers === this.compilers.length) {
					this.hooks.done.call(new MultiStats(compilerStats));
				}
			});
			// eslint-disable-next-line no-loop-func
			compiler.hooks.invalid.tap("MultiCompiler", () => {
				if (compilerDone) {
					compilerDone = false;
					doneCompilers--;
				}
			});
		}
		this.running = false;
	}

	get outputPath() {
		let commonPath = this.compilers[0].outputPath;
		for (const compiler of this.compilers) {
			while (
				compiler.outputPath.indexOf(commonPath) !== 0 &&
				/[/\\]/.test(commonPath)
			) {
				commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
			}
		}

		if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
		return commonPath;
	}

	get inputFileSystem() {
		throw new Error("Cannot read inputFileSystem of a MultiCompiler");
	}

	get outputFileSystem() {
		throw new Error("Cannot read outputFileSystem of a MultiCompiler");
	}

	set inputFileSystem(value) {
		for (const compiler of this.compilers) {
			compiler.inputFileSystem = value;
		}
	}

	set outputFileSystem(value) {
		for (const compiler of this.compilers) {
			compiler.outputFileSystem = value;
		}
	}

	validateDependencies(callback) {
		const edges = new Set();
		const missing = [];
		const targetFound = compiler => {
			for (const edge of edges) {
				if (edge.target === compiler) {
					return true;
				}
			}
			return false;
		};
		const sortEdges = (e1, e2) => {
			return (
				e1.source.name.localeCompare(e2.source.name) ||
				e1.target.name.localeCompare(e2.target.name)
			);
		};
		for (const source of this.compilers) {
			if (source.dependencies) {
				for (const dep of source.dependencies) {
					const target = this.compilers.find(c => c.name === dep);
					if (!target) {
						missing.push(dep);
					} else {
						edges.add({
							source,
							target
						});
					}
				}
			}
		}
		const errors = missing.map(m => `Compiler dependency \`${m}\` not found.`);
		const stack = this.compilers.filter(c => !targetFound(c));
		while (stack.length > 0) {
			const current = stack.pop();
			for (const edge of edges) {
				if (edge.source === current) {
					edges.delete(edge);
					const target = edge.target;
					if (!targetFound(target)) {
						stack.push(target);
					}
				}
			}
		}
		if (edges.size > 0) {
			const lines = Array.from(edges)
				.sort(sortEdges)
				.map(edge => `${edge.source.name} -> ${edge.target.name}`);
			lines.unshift("Circular dependency found in compiler dependencies.");
			errors.unshift(lines.join("\n"));
		}
		if (errors.length > 0) {
			const message = errors.join("\n");
			callback(new Error(message));
			return false;
		}
		return true;
	}

	runWithDependencies(compilers, fn, callback) {
		const fulfilledNames = new Set();
		let remainingCompilers = compilers;
		const isDependencyFulfilled = d => fulfilledNames.has(d);
		const getReadyCompilers = () => {
			let readyCompilers = [];
			let list = remainingCompilers;
			remainingCompilers = [];
			for (const c of list) {
				const ready =
					!c.dependencies || c.dependencies.every(isDependencyFulfilled);
				if (ready) {
					readyCompilers.push(c);
				} else {
					remainingCompilers.push(c);
				}
			}
			return readyCompilers;
		};
		const runCompilers = callback => {
			if (remainingCompilers.length === 0) return callback();
			asyncLib.map(
				getReadyCompilers(),
				(compiler, callback) => {
					fn(compiler, err => {
						if (err) return callback(err);
						fulfilledNames.add(compiler.name);
						runCompilers(callback);
					});
				},
				callback
			);
		};
		runCompilers(callback);
	}

	watch(watchOptions, handler) {
		if (this.running) return handler(new ConcurrentCompilationError());

		let watchings = [];
		let allStats = this.compilers.map(() => null);
		let compilerStatus = this.compilers.map(() => false);
		if (this.validateDependencies(handler)) {
			this.running = true;
			this.runWithDependencies(
				this.compilers,
				(compiler, callback) => {
					const compilerIdx = this.compilers.indexOf(compiler);
					let firstRun = true;
					let watching = compiler.watch(
						Array.isArray(watchOptions)
							? watchOptions[compilerIdx]
							: watchOptions,
						(err, stats) => {
							if (err) handler(err);
							if (stats) {
								allStats[compilerIdx] = stats;
								compilerStatus[compilerIdx] = "new";
								if (compilerStatus.every(Boolean)) {
									const freshStats = allStats.filter((s, idx) => {
										return compilerStatus[idx] === "new";
									});
									compilerStatus.fill(true);
									const multiStats = new MultiStats(freshStats);
									handler(null, multiStats);
								}
							}
							if (firstRun && !err) {
								firstRun = false;
								callback();
							}
						}
					);
					watchings.push(watching);
				},
				() => {
					// ignore
				}
			);
		}

		return new MultiWatching(watchings, this);
	}

	run(callback) {
		if (this.running) {
			return callback(new ConcurrentCompilationError());
		}

		const finalCallback = (err, stats) => {
			this.running = false;

			if (callback !== undefined) {
				return callback(err, stats);
			}
		};

		const allStats = this.compilers.map(() => null);
		if (this.validateDependencies(callback)) {
			this.running = true;
			this.runWithDependencies(
				this.compilers,
				(compiler, callback) => {
					const compilerIdx = this.compilers.indexOf(compiler);
					compiler.run((err, stats) => {
						if (err) {
							return callback(err);
						}
						allStats[compilerIdx] = stats;
						callback();
					});
				},
				err => {
					if (err) {
						return finalCallback(err);
					}
					finalCallback(null, new MultiStats(allStats));
				}
			);
		}
	}

	purgeInputFileSystem() {
		for (const compiler of this.compilers) {
			if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
				compiler.inputFileSystem.purge();
			}
		}
	}
};
<commit_msg>add getInfrastructureLogger to MultiCompiler<commit_after>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
"use strict";

const { Tapable, SyncHook, MultiHook } = require("tapable");
const asyncLib = require("neo-async");
const MultiWatching = require("./MultiWatching");
const MultiStats = require("./MultiStats");
const ConcurrentCompilationError = require("./ConcurrentCompilationError");

module.exports = class MultiCompiler extends Tapable {
	constructor(compilers) {
		super();
		this.hooks = {
			done: new SyncHook(["stats"]),
			invalid: new MultiHook(compilers.map(c => c.hooks.invalid)),
			run: new MultiHook(compilers.map(c => c.hooks.run)),
			watchClose: new SyncHook([]),
			watchRun: new MultiHook(compilers.map(c => c.hooks.watchRun))
		};
		if (!Array.isArray(compilers)) {
			compilers = Object.keys(compilers).map(name => {
				compilers[name].name = name;
				return compilers[name];
			});
		}
		this.compilers = compilers;
		let doneCompilers = 0;
		let compilerStats = [];
		let index = 0;
		for (const compiler of this.compilers) {
			let compilerDone = false;
			const compilerIndex = index++;
			// eslint-disable-next-line no-loop-func
			compiler.hooks.done.tap("MultiCompiler", stats => {
				if (!compilerDone) {
					compilerDone = true;
					doneCompilers++;
				}
				compilerStats[compilerIndex] = stats;
				if (doneCompilers === this.compilers.length) {
					this.hooks.done.call(new MultiStats(compilerStats));
				}
			});
			// eslint-disable-next-line no-loop-func
			compiler.hooks.invalid.tap("MultiCompiler", () => {
				if (compilerDone) {
					compilerDone = false;
					doneCompilers--;
				}
			});
		}
		this.running = false;
	}

	get outputPath() {
		let commonPath = this.compilers[0].outputPath;
		for (const compiler of this.compilers) {
			while (
				compiler.outputPath.indexOf(commonPath) !== 0 &&
				/[/\\]/.test(commonPath)
			) {
				commonPath = commonPath.replace(/[/\\][^/\\]*$/, "");
			}
		}

		if (!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
		return commonPath;
	}

	get inputFileSystem() {
		throw new Error("Cannot read inputFileSystem of a MultiCompiler");
	}

	get outputFileSystem() {
		throw new Error("Cannot read outputFileSystem of a MultiCompiler");
	}

	set inputFileSystem(value) {
		for (const compiler of this.compilers) {
			compiler.inputFileSystem = value;
		}
	}

	set outputFileSystem(value) {
		for (const compiler of this.compilers) {
			compiler.outputFileSystem = value;
		}
	}

	getInfrastructureLogger(name) {
		return this.compilers[0].getInfrastructureLogger(name);
	}

	validateDependencies(callback) {
		const edges = new Set();
		const missing = [];
		const targetFound = compiler => {
			for (const edge of edges) {
				if (edge.target === compiler) {
					return true;
				}
			}
			return false;
		};
		const sortEdges = (e1, e2) => {
			return (
				e1.source.name.localeCompare(e2.source.name) ||
				e1.target.name.localeCompare(e2.target.name)
			);
		};
		for (const source of this.compilers) {
			if (source.dependencies) {
				for (const dep of source.dependencies) {
					const target = this.compilers.find(c => c.name === dep);
					if (!target) {
						missing.push(dep);
					} else {
						edges.add({
							source,
							target
						});
					}
				}
			}
		}
		const errors = missing.map(m => `Compiler dependency \`${m}\` not found.`);
		const stack = this.compilers.filter(c => !targetFound(c));
		while (stack.length > 0) {
			const current = stack.pop();
			for (const edge of edges) {
				if (edge.source === current) {
					edges.delete(edge);
					const target = edge.target;
					if (!targetFound(target)) {
						stack.push(target);
					}
				}
			}
		}
		if (edges.size > 0) {
			const lines = Array.from(edges)
				.sort(sortEdges)
				.map(edge => `${edge.source.name} -> ${edge.target.name}`);
			lines.unshift("Circular dependency found in compiler dependencies.");
			errors.unshift(lines.join("\n"));
		}
		if (errors.length > 0) {
			const message = errors.join("\n");
			callback(new Error(message));
			return false;
		}
		return true;
	}

	runWithDependencies(compilers, fn, callback) {
		const fulfilledNames = new Set();
		let remainingCompilers = compilers;
		const isDependencyFulfilled = d => fulfilledNames.has(d);
		const getReadyCompilers = () => {
			let readyCompilers = [];
			let list = remainingCompilers;
			remainingCompilers = [];
			for (const c of list) {
				const ready =
					!c.dependencies || c.dependencies.every(isDependencyFulfilled);
				if (ready) {
					readyCompilers.push(c);
				} else {
					remainingCompilers.push(c);
				}
			}
			return readyCompilers;
		};
		const runCompilers = callback => {
			if (remainingCompilers.length === 0) return callback();
			asyncLib.map(
				getReadyCompilers(),
				(compiler, callback) => {
					fn(compiler, err => {
						if (err) return callback(err);
						fulfilledNames.add(compiler.name);
						runCompilers(callback);
					});
				},
				callback
			);
		};
		runCompilers(callback);
	}

	watch(watchOptions, handler) {
		if (this.running) return handler(new ConcurrentCompilationError());

		let watchings = [];
		let allStats = this.compilers.map(() => null);
		let compilerStatus = this.compilers.map(() => false);
		if (this.validateDependencies(handler)) {
			this.running = true;
			this.runWithDependencies(
				this.compilers,
				(compiler, callback) => {
					const compilerIdx = this.compilers.indexOf(compiler);
					let firstRun = true;
					let watching = compiler.watch(
						Array.isArray(watchOptions)
							? watchOptions[compilerIdx]
							: watchOptions,
						(err, stats) => {
							if (err) handler(err);
							if (stats) {
								allStats[compilerIdx] = stats;
								compilerStatus[compilerIdx] = "new";
								if (compilerStatus.every(Boolean)) {
									const freshStats = allStats.filter((s, idx) => {
										return compilerStatus[idx] === "new";
									});
									compilerStatus.fill(true);
									const multiStats = new MultiStats(freshStats);
									handler(null, multiStats);
								}
							}
							if (firstRun && !err) {
								firstRun = false;
								callback();
							}
						}
					);
					watchings.push(watching);
				},
				() => {
					// ignore
				}
			);
		}

		return new MultiWatching(watchings, this);
	}

	run(callback) {
		if (this.running) {
			return callback(new ConcurrentCompilationError());
		}

		const finalCallback = (err, stats) => {
			this.running = false;

			if (callback !== undefined) {
				return callback(err, stats);
			}
		};

		const allStats = this.compilers.map(() => null);
		if (this.validateDependencies(callback)) {
			this.running = true;
			this.runWithDependencies(
				this.compilers,
				(compiler, callback) => {
					const compilerIdx = this.compilers.indexOf(compiler);
					compiler.run((err, stats) => {
						if (err) {
							return callback(err);
						}
						allStats[compilerIdx] = stats;
						callback();
					});
				},
				err => {
					if (err) {
						return finalCallback(err);
					}
					finalCallback(null, new MultiStats(allStats));
				}
			);
		}
	}

	purgeInputFileSystem() {
		for (const compiler of this.compilers) {
			if (compiler.inputFileSystem && compiler.inputFileSystem.purge) {
				compiler.inputFileSystem.purge();
			}
		}
	}
};
<|endoftext|>


<commit_before>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
var Tapable = require("tapable");
var asyncLib = require("async");
var MultiWatching = require("./MultiWatching");
var MultiStats = require("./MultiStats");

function MultiCompiler(compilers) {
	Tapable.call(this);
	if(!Array.isArray(compilers)) {
		compilers = Object.keys(compilers).map(function(name) {
			compilers[name].name = name;
			return compilers[name];
		});
	}
	this.compilers = compilers;

	function delegateProperty(name) {
		Object.defineProperty(this, name, {
			configurable: false,
			get: function() {
				throw new Error("Cannot read " + name + " of a MultiCompiler");
			},
			set: function(value) {
				this.compilers.forEach(function(compiler) {
					compiler[name] = value;
				});
			}.bind(this)
		});
	}
	delegateProperty.call(this, "outputFileSystem");
	delegateProperty.call(this, "inputFileSystem");

	Object.defineProperty(this, "outputPath", {
		configurable: false,
		get: function() {
			var commonPath = compilers[0].outputPath;
			for(var i = 1; i < compilers.length; i++) {
				while(compilers[i].outputPath.indexOf(commonPath) !== 0 && /[\/\\]/.test(commonPath)) {
					commonPath = commonPath.replace(/[\/\\][^\/\\]*$/, "");
				}
			}
			if(!commonPath && compilers[0].outputPath[0] === "/") return "/";
			return commonPath;
		}
	});

	var doneCompilers = 0;
	var compilerStats = [];
	this.compilers.forEach(function(compiler, idx) {
		var compilerDone = false;
		compiler.plugin("done", function(stats) {
			if(!compilerDone) {
				compilerDone = true;
				doneCompilers++;
			}
			compilerStats[idx] = stats;
			if(doneCompilers === this.compilers.length) {
				this.applyPlugins("done", new MultiStats(compilerStats));
			}
		}.bind(this));
		compiler.plugin("invalid", function() {
			if(compilerDone) {
				compilerDone = false;
				doneCompilers--;
			}
			this.applyPlugins("invalid");
		}.bind(this));
	}, this);
}
module.exports = MultiCompiler;

MultiCompiler.prototype = Object.create(Tapable.prototype);
MultiCompiler.prototype.constructor = MultiCompiler;

function runWithDependencies(compilers, fn, callback) {
	var fulfilledNames = {};
	var remainingCompilers = compilers;

	function isDependencyFulfilled(d) {
		return fulfilledNames[d];
	}

	function getReadyCompilers() {
		var readyCompilers = [];
		var list = remainingCompilers;
		remainingCompilers = [];
		for(var i = 0; i < list.length; i++) {
			var c = list[i];
			var ready = !c.dependencies || c.dependencies.every(isDependencyFulfilled);
			if(ready)
				readyCompilers.push(c);
			else
				remainingCompilers.push(c);
		}
		return readyCompilers;
	}

	function runCompilers(callback) {
		if(remainingCompilers.length === 0) return callback();
		asyncLib.map(getReadyCompilers(), function(compiler, callback) {
			fn(compiler, function(err) {
				if(err) return callback(err);
				fulfilledNames[compiler.name] = true;
				runCompilers(callback);
			});
		}, callback);
	}
	runCompilers(callback);
}

MultiCompiler.prototype.watch = function(watchOptions, handler) {
	var watchings = [];
	var allStats = this.compilers.map(function() {
		return null;
	});
	var compilerStatus = this.compilers.map(function() {
		return false;
	});
	runWithDependencies(this.compilers, function(compiler, callback) {
		var compilerIdx = this.compilers.indexOf(compiler);
		var firstRun = true;
		var watching = compiler.watch(Array.isArray(watchOptions) ? watchOptions[compilerIdx] : watchOptions, function(err, stats) {
			if(err)
				handler(err);
			if(stats) {
				allStats[compilerIdx] = stats;
				compilerStatus[compilerIdx] = "new";
				if(compilerStatus.every(Boolean)) {
					var freshStats = allStats.filter(function(s, idx) {
						return compilerStatus[idx] === "new";
					});
					compilerStatus.fill(true);
					var multiStats = new MultiStats(freshStats);
					handler(null, multiStats);
				}
			}
			if(firstRun && !err) {
				firstRun = false;
				callback();
			}
		});
		watchings.push(watching);
	}.bind(this), function() {
		// ignore
	});

	return new MultiWatching(watchings, this);
};

MultiCompiler.prototype.run = function(callback) {
	var allStats = this.compilers.map(function() {
		return null;
	});

	runWithDependencies(this.compilers, function(compiler, callback) {
		var compilerIdx = this.compilers.indexOf(compiler);
		compiler.run(function(err, stats) {
			if(err) return callback(err);
			allStats[compilerIdx] = stats;
			callback();
		});
	}.bind(this), function(err) {
		if(err) return callback(err);
		callback(null, new MultiStats(allStats));
	});
};

MultiCompiler.prototype.purgeInputFileSystem = function() {
	this.compilers.forEach(function(compiler) {
		if(compiler.inputFileSystem && compiler.inputFileSystem.purge)
			compiler.inputFileSystem.purge();
	});
};
<commit_msg>4099_ES6 refactor lib/MultiCompiler.js<commit_after>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/
"use strict";

const Tapable = require("tapable");
const asyncLib = require("async");
const MultiWatching = require("./MultiWatching");
const MultiStats = require("./MultiStats");

module.exports = class MultiCompiler extends Tapable {
	constructor(compilers) {
		super();
		if(!Array.isArray(compilers)) {
			compilers = Object.keys(compilers).map((name) => {
				compilers[name].name = name;
				return compilers[name];
			});
		}
		this.compilers = compilers;
		let doneCompilers = 0;
		let compilerStats = [];
		this.compilers.forEach((compiler, idx) => {
			let compilerDone = false;
			compiler.plugin("done", stats => {
				if(!compilerDone) {
					compilerDone = true;
					doneCompilers++;
				}
				compilerStats[idx] = stats;
				if(doneCompilers === this.compilers.length) {
					this.applyPlugins("done", new MultiStats(compilerStats));
				}
			});
			compiler.plugin("invalid", () => {
				if(compilerDone) {
					compilerDone = false;
					doneCompilers--;
				}
				this.applyPlugins("invalid");
			});
		}, this);
	}

	get outputPath() {
		let commonPath = this.compilers[0].outputPath;
		for(const compiler of this.compilers) {
			while(compiler.outputPath.indexOf(commonPath) !== 0 && /[\/\\]/.test(commonPath)) {
				commonPath = commonPath.replace(/[\/\\][^\/\\]*$/, "");
			}
		}

		if(!commonPath && this.compilers[0].outputPath[0] === "/") return "/";
		return commonPath;
	}

	get inputFileSystem() {
		throw new Error("Cannot read inputFileSystem of a MultiCompiler");
	}

	get outputFileSystem() {
		throw new Error("Cannot read outputFileSystem of a MultiCompiler");
	}

	set inputFileSystem(value) {
		this.compilers.forEach(compiler => {
			compiler.inputFileSystem = value;
		});
	}

	set outputFileSystem(value) {
		this.compilers.forEach(compiler => {
			compiler.outputFileSystem = value;
		});
	}

	runWithDependencies(compilers, fn, callback) {
		let fulfilledNames = {};
		let remainingCompilers = compilers;
		const isDependencyFulfilled = (d) => fulfilledNames[d];
		const getReadyCompilers = () => {
			let readyCompilers = [];
			let list = remainingCompilers;
			remainingCompilers = [];
			for(const c of list) {
				const ready = !c.dependencies || c.dependencies.every(isDependencyFulfilled);
				if(ready)
					readyCompilers.push(c);
				else
					remainingCompilers.push(c);
			}
			return readyCompilers;
		};
		const runCompilers = (callback) => {
			if(remainingCompilers.length === 0) return callback();
			asyncLib.map(getReadyCompilers(), (compiler, callback) => {
				fn(compiler, (err) => {
					if(err) return callback(err);
					fulfilledNames[compiler.name] = true;
					runCompilers(callback);
				});
			}, callback);
		};
		runCompilers(callback);
	}

	watch(watchOptions, handler) {
		let watchings = [];
		let allStats = this.compilers.map(() => null);
		let compilerStatus = this.compilers.map(() => false);
		this.runWithDependencies(this.compilers, (compiler, callback) => {
			const compilerIdx = this.compilers.indexOf(compiler);
			let firstRun = true;
			let watching = compiler.watch(Array.isArray(watchOptions) ? watchOptions[compilerIdx] : watchOptions, (err, stats) => {
				if(err)
					handler(err);
				if(stats) {
					allStats[compilerIdx] = stats;
					compilerStatus[compilerIdx] = "new";
					if(compilerStatus.every(Boolean)) {
						const freshStats = allStats.filter((s, idx) => {
							return compilerStatus[idx] === "new";
						});
						compilerStatus.fill(true);
						const multiStats = new MultiStats(freshStats);
						handler(null, multiStats);
					}
				}
				if(firstRun && !err) {
					firstRun = false;
					callback();
				}
			});
			watchings.push(watching);
		}, () => {
			// ignore
		});

		return new MultiWatching(watchings, this);
	}

	run(callback) {
		const allStats = this.compilers.map(() => null);
		this.runWithDependencies(this.compilers, ((compiler, callback) => {
			const compilerIdx = this.compilers.indexOf(compiler);
			compiler.run((err, stats) => {
				if(err) return callback(err);
				allStats[compilerIdx] = stats;
				callback();
			});
		}), (err) => {
			if(err) return callback(err);
			callback(null, new MultiStats(allStats));
		});
	}

	purgeInputFileSystem() {
		this.compilers.forEach((compiler) => {
			if(compiler.inputFileSystem && compiler.inputFileSystem.purge)
				compiler.inputFileSystem.purge();
		});
	}
};
<|endoftext|>


<commit_before>/* ---------------------------------------------------- +/

## Location ##

Code related to the location template

/+ ---------------------------------------------------- */

//Template.location.created = function () {
//};

//Template.location.helpers({
    //myHelper: function () {
    //}
//});

Template.location.rendered = function () {
    // Fade in the main location wrapper
    $('.location').addClass('animated fadeIn92');

    /**
     * Setup image area
     */
    var width = 1800,
        height = 1080 - 100,
        svg = d3.select(".images").append("svg")
            .attr("class", 'svg-canvas')
            .attr("width", width)
            .attr("height", height);

    /**
     * Setup timeline area
     */
    var time = d3.select(".time").append("svg")
        .attr("class", 'time-canvas')
        .attr("width", width - 30)
        .attr("height", 300);

    /**
     * Setup big picture area
     */

    //bigPicSVG = d3.select(".big-picture").append("svg")
        //.attr("class", 'svg-big-picture')
        //.attr("width", 800)
        //.attr("height", 600);

    /**
     * We have to do our D3 stuff in here because of Meteor's client server
     * stuff. I don't exactly understand...
     *
     * TODO: Research
     */
    Deps.autorun(function () {
        /**
         * Wait for a half-second to let the collections load.
         *
         * TODO: figure out why we need to do this. Before I was doing this,
         * the image counts would get set three times with successively
         * increasing numbers.
         */
        window.setTimeout(function() {
            renderLocation();
        }, 500);

        function renderLocation() {
            /**
             * Render each image
             */
            var imagesCursor = Images.find();
            var imagesCount = imagesCursor.count();
            var images = imagesCursor.fetch();
            images = _.sortBy(images, function(image) {
                return image.date;
            });
            var timelineMargin = 60;
            widthInterval = parseInt((width - timelineMargin) / imagesCount);
            var firstImageWidth = images[0].thumbWidth;
            var lastImageWidth = images[parseInt(imagesCount - 1)].thumbWidth;
            _.each(images, function(image, i) {
                drawImage(svg, image, i, imagesCount, firstImageWidth, lastImageWidth, timelineMargin, widthInterval);
            });

            /**
             * Print the start and end years of the timeline
             */
            // Start
            time.append('svg:text')
                .attr("x", 10)
                .attr("y", 35)
                .attr("class", 'time-label-start')
                .text('1800');

            // End
            time.append('svg:text')
                .attr("x", 1675)
                .attr("y", 35)
                .attr("class", 'time-label-end')
                .text('2000');

            /**
             * Render the timeline handle
             */
            time.append('rect')
                .attr("width", "50")
                .attr("height", "50")
                .attr('class', 'time-handle-rect')
                .attr("x", 0)
                .attr("y", 0);

        };
    });

    /**
     * Render each image
     */
    function drawImage(svg, image, i, imagesCount, firstImageWidth, lastImageWidth, timelineMargin, widthInterval) {
        /**
         * Image properties
         */
        var imageBorder = 5;
        var delay = 2; // Milliseconds to delay the animation per image
        var dur = 500; // Milliseconds for the image animation

        /**
         * Image positioning
         */
        // The x calue for the first image at the left edge of the timeline
        var leftEdge = (timelineMargin / 2) - imageBorder;
        var centerX;
        if (i === 0) {
            centerX = leftEdge;
        }

        // The x value for the last image at the right edge of the timeline
        //
        // The last values account for the shadow offset in the group
        var rightEdge = (timelineMargin / 2) + 1760 - lastImageWidth - (imageBorder * 2) - 5;
        if (i == (imagesCount - 1)){
            centerX = rightEdge;
        }

        // Find the proper interval between images, for the rest of the images
        var rightEdgeCenter = rightEdge + (firstImageWidth / 2);
        var centerInterval = (rightEdgeCenter - leftEdge) / (imagesCount - 2);

        if ((i !== 0) && (i != (imagesCount - 1))) {
            // 15 for white border and shadow
            centerX = leftEdge + (centerInterval * i) - ((image.thumbWidth + 15) / 2);
        }

        // Put the images at the top of the timeline
        var bottomY = (835 - image.thumbHeight);
        var translate = 'translate(' + centerX + ',' + bottomY + ')';

        /**
         * Picture group parent
         */
        var pictureGroup = svg.append("g")
            .attr('class', 'picture-group ' + 'picture-' + i)
            .attr('data-index', i)
            .attr('data-id', image._id)
            .attr('data-date', image.date)
            .attr('data-title', image.title)
            .attr('data-xw', image.expandedWidth)
            .attr('data-xh', image.expandedHeight)
            .attr('data-description', image.description)
            .attr("transform", function (){
                return translate;
            });

            /**
            * Picture drop shadow
            */
            var filter = pictureGroup.append("defs")
                .append("filter")
                .attr("id", "blur")
                .append("feGaussianBlur")
                .attr("stdDeviation", 5);
            pictureGroup.append('rect')
                .attr("x", 0)
                .attr("y", 0)
                .style('fill', '#000')
                .attr("width", image.thumbWidth + (imageBorder * 2))
                .attr("height", image.thumbHeight + (imageBorder * 2))
                .attr('class', 'child')
                .attr("filter", "url(#blur)");

            /**
            * Picture white border
            */
            pictureGroup.append('rect')
                // Positions are relative to the group
                .attr("x", (0 - imageBorder))
                .attr("y", (0 - imageBorder))
                .attr("width", image.thumbWidth + (imageBorder * 2))
                .attr("height", image.thumbHeight + (imageBorder * 2))
                .attr('class', 'child location-matte');

            // Image
            pictureGroup.append('image')
                .attr("xlink:href", "/images/thumbnails/" + image._id + ".jpg")
                .attr("data-id", image._id)
                .attr("data-location", image.generalLocationDs)
                //// Simulate scaling form the center of the image
                .attr("width", image.thumbWidth)
                .attr("height", image.thumbHeight)
                .attr('location', image.generalLocationDs)
                .attr('class', 'child');

        /**
         * Picture starting state
         *
         * Scale at 0
         * All child elements with an opacity of 0
         */
        pictureGroup
            .attr("transform", function (){
                transform = translate + ',scale(0)';
                return transform;
            });
        pictureGroup.selectAll('.child')
            .attr("opacity", "0");

        /**
         * Picture - Animate in.
         *
         * Scale and opacity at 1
         */
        pictureGroup
            .transition()
            .delay(i * delay) // Stagger the markers animating in
            .attr("transform", function (){
                transform = translate + ',scale(1)';
                return transform;
            })
            .duration(dur);
        pictureGroup.selectAll('.child')
            .transition()
            .delay(i * delay) // Stagger the markers animating in
            .attr("opacity", "1")
            .duration(dur);

    }
};

Template.location.events({
    // Desired functionality, but disabled for testing
    'mousemove .container': function (e) {
    //'click .container': function (e) {

        /**
         * Setup basic objects and widths
         */
        var timeline = $('.time-canvas');

        // Count elements in our SVG element to get number of images
        var imagesCount = d3.selectAll('.picture-group')[0].length;

        // Pixel interval between images
        var intervalWidth = (timeline.width() / imagesCount);

        // Mouse position relative to the timeline
        var posX = e.pageX - timeline.parent().offset().left;
        var posInterval = Math.floor(posX / intervalWidth);

        /**
         * Position the handle relative to our pointer event
         */
        var handle = d3.select('.time-handle-rect');
        var handleWidth = handle.attr('width');
        var handleWidthHalf = (handleWidth / 2);

        // Prevent the handle from going off the edge of the timeline
        if (posX < handleWidthHalf) {
            posX = (handleWidthHalf);
            posInterval = 1;
        }
        if (posX > 1760 - handleWidthHalf) {
            posX = (1760 - handleWidthHalf);
            posInterval = imagesCount;
        }

        // Set the middle of the handle to the mouse position
        handle.attr('x', (posX - handleWidthHalf));

        /**
         * Scale the images based on mouse position
         *
         * Make the images nearest the cursor the biggest
         */
        d3.selectAll('.picture-group').each( function(d, i){

            /**
             * Determine a scale value based on mouse position
             */
            i = Number(d3.select(this).attr("data-index"));
            var distance = posInterval - i;
            //var distanceScale;
            if (distance === 0) {
                distanceScale = 1;
            }
            else {
                var minVal = 0.3;
                var maxVal = 0.7;
                distanceScale = ( minVal + (maxVal - minVal) * (1 / (Math.abs(posInterval - i))));
            }

            /**
             * Transform the picture group
             */
            var pictureGroup = d3.select(this);
            var imageInGroup = pictureGroup.select('image');
            var imageHeight = imageInGroup
                .attr('height');
            var imageWidth = imageInGroup
                .attr('width');
            // Get the current transform object
            var t = d3.transform(pictureGroup.attr('transform'));
            // Set the scale value, without changing other attributes
            // This allows the image to stay at its current X,Y position
            // while scaling.
            t.scale = [distanceScale, distanceScale];
            var translateY;
            if (posInterval == i) {
                translateY = (835 - (imageHeight * distanceScale)) - 50;
            }
            else {
                translateY = (835 - (imageHeight * distanceScale));
            }
            // TODO: Figure out how to get the X values to scale with the scale() value
            //
            //var translateX = t.translate[0] + ((imageWidth / 2) * distanceScale);
            //var translateX = t.translate[0] + ((imageWidth / 2) * distanceScale);
            var translateX = t.translate[0];
            t.translate = [
                translateX, // Keep the X axis in placename
                translateY
            ];
            //console.log('t translate Y', t.translate[1]);
            // Turn the transform back into a string for SVG
            transformString = t.toString();
            pictureGroup
                .transition()
                .duration(100)
                .ease('circle-out')
                .attr("transform", transformString);
        });

        /**
         * Display detail information about the photograph
         */
        // Get the image data from the thumbnail data objects
        hlImg = $('g[data-index=' + posInterval + ']');

        // Set the title
        hlImgTitle = hlImg.data('title');
        $('.image-detail h4').text(hlImgTitle);

        hlImgDescription = hlImg.data('description');
        $('.image-detail span.image-description').text(hlImgDescription);

        hlImgId = hlImg.data('id');
        hlImgExWidth = hlImg.data('xw');
        console.log('w', hlImgExWidth);
        hlImgExHeight = hlImg.data('xh');
        console.log('h', hlImgExHeight);
        //bigPic = d3.select("g.big-picture");
        $('.big-picture-image').attr('src', '/images/expanded/' + hlImgId + ".jpg");
        $('.big-picture-image').attr('height', (parseInt(hlImgExHeight) / 2));

        /**
         * Dev mouse position data
         */
        $('.dev-mouse').html(
            'Rel pointer X = ' + posX + '
' +
            'Interval width = ' + intervalWidth + '
' +
            'Pos interval = ' + posInterval + '
'
        );

    },

    'click .back': function(e, instance){
        console.clear();
    },

    'click .delete': function(e, instance){
        var location = this;
        e.preventDefault();
        Meteor.call('removeLocation', location, function(error, result){
            alert('Location deleted.');
            Router.go('/locations');
        });
    }

});
<commit_msg>Prevent image change flickrs<commit_after>/* ---------------------------------------------------- +/

## Location ##

Code related to the location template

/+ ---------------------------------------------------- */

//Template.location.created = function () {
//};

//Template.location.helpers({
    //myHelper: function () {
    //}
//});

Template.location.rendered = function () {
    // Fade in the main location wrapper
    $('.location').addClass('animated fadeIn92');

    /**
     * Setup image area
     */
    var width = 1800,
        height = 1080 - 100,
        svg = d3.select(".images").append("svg")
            .attr("class", 'svg-canvas')
            .attr("width", width)
            .attr("height", height);

    /**
     * Setup timeline area
     */
    var time = d3.select(".time").append("svg")
        .attr("class", 'time-canvas')
        .attr("width", width - 30)
        .attr("height", 300);

    /**
     * Setup big picture area
     */

    //bigPicSVG = d3.select(".big-picture").append("svg")
        //.attr("class", 'svg-big-picture')
        //.attr("width", 800)
        //.attr("height", 600);

    /**
     * We have to do our D3 stuff in here because of Meteor's client server
     * stuff. I don't exactly understand...
     *
     * TODO: Research
     */
    Deps.autorun(function () {
        /**
         * Wait for a half-second to let the collections load.
         *
         * TODO: figure out why we need to do this. Before I was doing this,
         * the image counts would get set three times with successively
         * increasing numbers.
         */
        window.setTimeout(function() {
            renderLocation();
        }, 500);

        function renderLocation() {
            /**
             * Render each image
             */
            var imagesCursor = Images.find();
            var imagesCount = imagesCursor.count();
            var images = imagesCursor.fetch();
            images = _.sortBy(images, function(image) {
                return image.date;
            });
            var timelineMargin = 60;
            widthInterval = parseInt((width - timelineMargin) / imagesCount);
            var firstImageWidth = images[0].thumbWidth;
            var lastImageWidth = images[parseInt(imagesCount - 1)].thumbWidth;
            _.each(images, function(image, i) {
                drawImage(svg, image, i, imagesCount, firstImageWidth, lastImageWidth, timelineMargin, widthInterval);
            });

            /**
             * Print the start and end years of the timeline
             */
            // Start
            time.append('svg:text')
                .attr("x", 10)
                .attr("y", 35)
                .attr("class", 'time-label-start')
                .text('1800');

            // End
            time.append('svg:text')
                .attr("x", 1675)
                .attr("y", 35)
                .attr("class", 'time-label-end')
                .text('2000');

            /**
             * Render the timeline handle
             */
            time.append('rect')
                .attr("width", "50")
                .attr("height", "50")
                .attr('class', 'time-handle-rect')
                .attr("x", 0)
                .attr("y", 0);

        };
    });

    /**
     * Render each image
     */
    function drawImage(svg, image, i, imagesCount, firstImageWidth, lastImageWidth, timelineMargin, widthInterval) {
        /**
         * Image properties
         */
        var imageBorder = 5;
        var delay = 2; // Milliseconds to delay the animation per image
        var dur = 500; // Milliseconds for the image animation

        /**
         * Image positioning
         */
        // The x calue for the first image at the left edge of the timeline
        var leftEdge = (timelineMargin / 2) - imageBorder;
        var centerX;
        if (i === 0) {
            centerX = leftEdge;
        }

        // The x value for the last image at the right edge of the timeline
        //
        // The last values account for the shadow offset in the group
        var rightEdge = (timelineMargin / 2) + 1760 - lastImageWidth - (imageBorder * 2) - 5;
        if (i == (imagesCount - 1)){
            centerX = rightEdge;
        }

        // Find the proper interval between images, for the rest of the images
        var rightEdgeCenter = rightEdge + (firstImageWidth / 2);
        var centerInterval = (rightEdgeCenter - leftEdge) / (imagesCount - 2);

        if ((i !== 0) && (i != (imagesCount - 1))) {
            // 15 for white border and shadow
            centerX = leftEdge + (centerInterval * i) - ((image.thumbWidth + 15) / 2);
        }

        // Put the images at the top of the timeline
        var bottomY = (835 - image.thumbHeight);
        var translate = 'translate(' + centerX + ',' + bottomY + ')';

        /**
         * Picture group parent
         */
        var pictureGroup = svg.append("g")
            .attr('class', 'picture-group ' + 'picture-' + i)
            .attr('data-index', i)
            .attr('data-id', image._id)
            .attr('data-date', image.date)
            .attr('data-title', image.title)
            .attr('data-xw', image.expandedWidth)
            .attr('data-xh', image.expandedHeight)
            .attr('data-description', image.description)
            .attr("transform", function (){
                return translate;
            });

            /**
            * Picture drop shadow
            */
            var filter = pictureGroup.append("defs")
                .append("filter")
                .attr("id", "blur")
                .append("feGaussianBlur")
                .attr("stdDeviation", 5);
            pictureGroup.append('rect')
                .attr("x", 0)
                .attr("y", 0)
                .style('fill', '#000')
                .attr("width", image.thumbWidth + (imageBorder * 2))
                .attr("height", image.thumbHeight + (imageBorder * 2))
                .attr('class', 'child')
                .attr("filter", "url(#blur)");

            /**
            * Picture white border
            */
            pictureGroup.append('rect')
                // Positions are relative to the group
                .attr("x", (0 - imageBorder))
                .attr("y", (0 - imageBorder))
                .attr("width", image.thumbWidth + (imageBorder * 2))
                .attr("height", image.thumbHeight + (imageBorder * 2))
                .attr('class', 'child location-matte');

            // Image
            pictureGroup.append('image')
                .attr("xlink:href", "/images/thumbnails/" + image._id + ".jpg")
                .attr("data-id", image._id)
                .attr("data-location", image.generalLocationDs)
                //// Simulate scaling form the center of the image
                .attr("width", image.thumbWidth)
                .attr("height", image.thumbHeight)
                .attr('location', image.generalLocationDs)
                .attr('class', 'child');

        /**
         * Picture starting state
         *
         * Scale at 0
         * All child elements with an opacity of 0
         */
        pictureGroup
            .attr("transform", function (){
                transform = translate + ',scale(0)';
                return transform;
            });
        pictureGroup.selectAll('.child')
            .attr("opacity", "0");

        /**
         * Picture - Animate in.
         *
         * Scale and opacity at 1
         */
        pictureGroup
            .transition()
            .delay(i * delay) // Stagger the markers animating in
            .attr("transform", function (){
                transform = translate + ',scale(1)';
                return transform;
            })
            .duration(dur);
        pictureGroup.selectAll('.child')
            .transition()
            .delay(i * delay) // Stagger the markers animating in
            .attr("opacity", "1")
            .duration(dur);

    }
};

Template.location.events({
    // Desired functionality, but disabled for testing
    'mousemove .container': function (e) {
    //'click .container': function (e) {

        /**
         * Setup basic objects and widths
         */
        var timeline = $('.time-canvas');

        // Count elements in our SVG element to get number of images
        var imagesCount = d3.selectAll('.picture-group')[0].length;

        // Pixel interval between images
        var intervalWidth = (timeline.width() / imagesCount);

        // Mouse position relative to the timeline
        var posX = e.pageX - timeline.parent().offset().left;
        var posInterval = Math.floor(posX / intervalWidth);

        /**
         * Position the handle relative to our pointer event
         */
        var handle = d3.select('.time-handle-rect');
        var handleWidth = handle.attr('width');
        var handleWidthHalf = (handleWidth / 2);

        // Prevent the handle from going off the edge of the timeline
        if (posX < handleWidthHalf) {
            posX = (handleWidthHalf);
            posInterval = 1;
        }
        if (posX > 1760 - handleWidthHalf) {
            posX = (1760 - handleWidthHalf);
            posInterval = imagesCount;
        }

        // Set the middle of the handle to the mouse position
        handle.attr('x', (posX - handleWidthHalf));

        /**
         * Scale the images based on mouse position
         *
         * Make the images nearest the cursor the biggest
         */
        d3.selectAll('.picture-group').each( function(d, i){

            /**
             * Determine a scale value based on mouse position
             */
            i = Number(d3.select(this).attr("data-index"));
            var distance = posInterval - i;
            //var distanceScale;
            if (distance === 0) {
                distanceScale = 1;
            }
            else {
                var minVal = 0.3;
                var maxVal = 0.7;
                distanceScale = ( minVal + (maxVal - minVal) * (1 / (Math.abs(posInterval - i))));
            }

            /**
             * Transform the picture group
             */
            var pictureGroup = d3.select(this);
            var imageInGroup = pictureGroup.select('image');
            var imageHeight = imageInGroup
                .attr('height');
            var imageWidth = imageInGroup
                .attr('width');
            // Get the current transform object
            var t = d3.transform(pictureGroup.attr('transform'));
            // Set the scale value, without changing other attributes
            // This allows the image to stay at its current X,Y position
            // while scaling.
            t.scale = [distanceScale, distanceScale];
            var translateY;
            if (posInterval == i) {
                translateY = (835 - (imageHeight * distanceScale)) - 50;
            }
            else {
                translateY = (835 - (imageHeight * distanceScale));
            }
            // TODO: Figure out how to get the X values to scale with the scale() value
            //
            //var translateX = t.translate[0] + ((imageWidth / 2) * distanceScale);
            //var translateX = t.translate[0] + ((imageWidth / 2) * distanceScale);
            var translateX = t.translate[0];
            t.translate = [
                translateX, // Keep the X axis in placename
                translateY
            ];
            //console.log('t translate Y', t.translate[1]);
            // Turn the transform back into a string for SVG
            transformString = t.toString();
            pictureGroup
                .transition()
                .duration(100)
                .ease('circle-out')
                .attr("transform", transformString);
        });

        /**
         * Display detail information about the photograph
         */
        // Get the image data from the thumbnail data objects
        hlImg = $('g[data-index=' + posInterval + ']');

        // Set the title
        hlImgTitle = hlImg.data('title');
        $('.image-detail h4').text(hlImgTitle);

        hlImgDescription = hlImg.data('description');
        $('.image-detail span.image-description').text(hlImgDescription);

        hlImgId = hlImg.data('id');
        hlImgExWidth = hlImg.data('xw');
        console.log('w', hlImgExWidth);
        hlImgExHeight = hlImg.data('xh');
        console.log('h', hlImgExHeight);
        //bigPic = d3.select("g.big-picture");

        // Only change the image when we need to
        var imagePath = '/images/expanded/' + hlImgId + '.jpg';
        if ($('.big-picture-image').attr('src') != imagePath) {
            $('.big-picture-image').attr('src', imagePath).stop(true, true).hide().fadeIn(400);
            $('.big-picture-image').attr('height', (parseInt(hlImgExHeight) / 2));
        }

        /**
         * Dev mouse position data
         */
        $('.dev-mouse').html(
            'Rel pointer X = ' + posX + '
' +
            'Interval width = ' + intervalWidth + '
' +
            'Pos interval = ' + posInterval + '
'
        );

    },

    'click .back': function(e, instance){
        console.clear();
    },

    'click .delete': function(e, instance){
        var location = this;
        e.preventDefault();
        Meteor.call('removeLocation', location, function(error, result){
            alert('Location deleted.');
            Router.go('/locations');
        });
    }

});
<|endoftext|>


<commit_before>const _ = require('lodash');
const utils = require('./utils');
const objection = require('objection');
const graphqlRoot = require('graphql');

const jsonSchemaUtils = require('./jsonSchema');
const defaultArgFactories = require('./argFactories');

const { GraphQLObjectType, GraphQLSchema, GraphQLList } = graphqlRoot;

// Default arguments that are excluded from the relation arguments.
const OMIT_FROM_RELATION_ARGS = [
  // We cannot use `range` in the relation arguments since the relations are fetched
  // for multiple objects at a time. Limiting the result set would limit the combined
  // result, and not the individual model's relation.
  'range',
];

const GRAPHQL_META_FIELDS = [
  '__typename',
];

// GraphQL AST node types.
const KIND_FRAGMENT_SPREAD = 'FragmentSpread';
const KIND_VARIABLE = 'Variable';

class SchemaBuilder {
  constructor() {
    this.models = {};
    this.typeCache = {};
    this.filterIndex = 1;
    this.argFactories = [];
    this.enableSelectFiltering = true;
    this.defaultArgNameMap = {
      eq: 'Eq',
      gt: 'Gt',
      gte: 'Gte',
      lt: 'Lt',
      lte: 'Lte',
      like: 'Like',
      isNull: 'IsNull',
      likeNoCase: 'LikeNoCase',
      in: 'In',
      notIn: 'NotIn',
      orderBy: 'orderBy',
      orderByDesc: 'orderByDesc',
      range: 'range',
    };
  }

  model(modelClass, opt) {
    opt = opt || {};

    if (!modelClass.jsonSchema) {
      throw new Error('modelClass must have a jsonSchema');
    }

    this.models[modelClass.tableName] = {
      modelClass,
      fields: null,
      args: null,
      opt,
    };

    return this;
  }

  models(models) {
    models.forEach(model => this.model(model))
  }

  defaultArgNames(defaultArgNameMap) {
    this.defaultArgNameMap = Object.assign({}, this.defaultArgNameMap, defaultArgNameMap);
    return this;
  }

  argFactory(argFactory) {
    this.argFactories.push(argFactory);
    return this;
  }

  selectFiltering(enable) {
    this.enableSelectFiltering = !!enable;
    return this;
  }

  extendWithMutations(mutations) {
    if (!(mutations instanceof GraphQLObjectType || mutations instanceof Function)) {
      throw new TypeError('mutations should be a function or an object of type GraphQLObjectType');
    }

    this.mutation = mutations;
    return this;
  }

  setBuilderOptions(options) {
    this.builderOptions = options;
    return this;
  }

  build() {
    _.forOwn(this.models, (modelData) => {
      modelData.fields = jsonSchemaUtils.jsonSchemaToGraphQLFields(modelData.modelClass.jsonSchema, {
        include: modelData.opt.include,
        exclude: modelData.opt.exclude,
        typeNamePrefix: utils.typeNameForModel(modelData.modelClass),
        typeCache: this.typeCache,
      });

      modelData.args = this._argsForModel(modelData);
    });

    const schemaSetup = {
      query: new GraphQLObjectType({
        name: 'Query',
        fields: () => {
          const fields = {};

          _.forOwn(this.models, (modelData) => {
            const defaultFieldName = fieldNameForModel(modelData.modelClass);
            const singleFieldName = modelData.opt.fieldName || defaultFieldName;
            const listFieldName = modelData.opt.listFieldName || (`${defaultFieldName}s`);

            fields[singleFieldName] = this._rootSingleField(modelData);
            fields[listFieldName] = this._rootListField(modelData);
          });

          return fields;
        },
      }),
    };

    if (this.mutation) {
      if (this.mutation instanceof Function) {
        schemaSetup.mutation = this.mutation(this);
      } else {
        schemaSetup.mutation = this.mutation;
      }
    }

    return new GraphQLSchema(schemaSetup);
  }

  _argsForModel(modelData) {
    const factories = defaultArgFactories(this.defaultArgNameMap, { typeCache: this.typeCache }).concat(this.argFactories);

    return factories.reduce((args, factory) => Object.assign(args, factory(modelData.fields, modelData.modelClass)), {});
  }

  _rootSingleField(modelData) {
    return {
      type: this._typeForModel(modelData),
      args: modelData.args,
      resolve: this._resolverForModel(modelData, (query) => {
        query.first();
      }),
    };
  }

  _rootListField(modelData) {
    return {
      type: new GraphQLList(this._typeForModel(modelData)),
      args: modelData.args,
      resolve: this._resolverForModel(modelData),
    };
  }

  _typeForModel(modelData) {
    const typeName = utils.typeNameForModel(modelData.modelClass);

    if (!this.typeCache[typeName]) {
      this.typeCache[typeName] = new GraphQLObjectType({
        name: typeName,
        fields: () => Object.assign({}, this._attrFields(modelData), this._relationFields(modelData)),
      });
    }

    return this.typeCache[typeName];
  }

  _attrFields(modelData) {
    return modelData.fields;
  }

  _relationFields(modelData) {
    const fields = {};

    _.forOwn(modelData.modelClass.getRelations(), (relation) => {
      const relationModel = this.models[relation.relatedModelClass.tableName];

      if (!relationModel) {
        // If the relation model has not been given for the builder using `model()` method
        // we don't handle the relations that have that class.
        return;
      }

      if (utils.isExcluded(relationModel.opt, relation.name)) {
        // If the property by the relation's name has been excluded, skip this relation.
        return;
      }

      fields[relation.name] = this._relationField(relationModel, relation);
    });

    return fields;
  }

  _relationField(modelData, relation) {
    if (relation instanceof objection.HasOneRelation
        || relation instanceof objection.BelongsToOneRelation
        || relation instanceof objection.HasOneThroughRelation) {
      return {
        type: this._typeForModel(modelData),
        args: _.omit(modelData.args, OMIT_FROM_RELATION_ARGS),
      };
    } else if (relation instanceof objection.HasManyRelation || relation instanceof objection.ManyToManyRelation) {
      return {
        type: new GraphQLList(this._typeForModel(modelData)),
        args: _.omit(modelData.args, OMIT_FROM_RELATION_ARGS),
      };
    }
    throw new Error(`relation type "${relation.constructor.name}" is not supported`);
  }

  _resolverForModel(modelData, extraQuery) {
    return (ctx, ignore1, ignore2, data) => {
      ctx = ctx || {};

      const { modelClass } = modelData;
      const ast = (data.fieldASTs || data.fieldNodes)[0];
      const eager = this._buildEager(ast, modelClass, data);
      const argFilter = this._filterForArgs(ast, modelClass, data.variableValues);
      const selectFilter = this._filterForSelects(ast, modelClass, data);
      const builder = modelClass.query(ctx.knex);

      if (this.builderOptions && this.builderOptions.skipUndefined) {
        builder.skipUndefined();
      }

      if (ctx.onQuery) {
        ctx.onQuery(builder, ctx);
      }

      if (argFilter) {
        builder.modify(argFilter);
      }

      if (selectFilter) {
        builder.modify(selectFilter);
      }

      if (extraQuery) {
        builder.modify(extraQuery);
      }

      if (eager.expression) {
        builder.eager(eager.expression, eager.filters);
      }

      return builder.then(toJson);
    };
  }

  _buildEager(astNode, modelClass, astRoot) {
    const eagerExpr = this._buildEagerSegment(astNode, modelClass, astRoot);

    if (eagerExpr.expression.length) {
      eagerExpr.expression = `[${eagerExpr.expression}]`;
    }

    return eagerExpr;
  }

  _buildEagerSegment(astNode, modelClass, astRoot) {
    const filters = {};
    const relations = modelClass.getRelations();
    let expression = '';

    for (let i = 0, l = astNode.selectionSet.selections.length; i < l; i += 1) {
      const selectionNode = astNode.selectionSet.selections[i];
      const relation = relations[selectionNode.name.value];

      if (relation) {
        expression = this._buildEagerRelationSegment(selectionNode, relation, expression, filters, astRoot);
      } else if (selectionNode.kind === KIND_FRAGMENT_SPREAD) {
        expression = this._buildEagerFragmentSegment(selectionNode, modelClass, expression, filters, astRoot);
      }
    }

    return {
      expression,
      filters,
    };
  }

  _buildEagerRelationSegment(selectionNode, relation, expression, filters, astRoot) {
    let relExpr = selectionNode.name.value;

    const selectFilter = this._filterForSelects(selectionNode, relation.relatedModelClass, astRoot);
    const filterNames = [];

    if (selectFilter) {
      this.filterIndex += 1;
      const filterName = `s${this.filterIndex}`;

      filterNames.push(filterName);
      filters[filterName] = selectFilter;
    }

    if (selectionNode.arguments.length) {
      const argFilter = this._filterForArgs(selectionNode, relation.relatedModelClass, astRoot.variableValues);

      if (argFilter) {
        this.filterIndex += 1;
        const filterName = `f${this.filterIndex}`;

        filterNames.push(filterName);
        filters[filterName] = argFilter;
      }
    }

    if (filterNames.length) {
      relExpr += `(${filterNames.join(', ')})`;
    }

    const subExpr = this._buildEager(selectionNode, relation.relatedModelClass, astRoot);

    if (subExpr.expression.length) {
      relExpr += `.${subExpr.expression}`;
      Object.assign(filters, subExpr.filters);
    }

    if (expression.length) {
      expression += ', ';
    }

    return expression + relExpr;
  }

  _buildEagerFragmentSegment(selectionNode, modelClass, expression, filters, astRoot) {
    const fragmentSelection = astRoot.fragments[selectionNode.name.value];
    const fragmentExpr = this._buildEagerSegment(fragmentSelection, modelClass, astRoot);
    let fragmentExprString = '';

    if (fragmentExpr.expression.length) {
      fragmentExprString += fragmentExpr.expression;
      Object.assign(filters, fragmentExpr.filters);
    }

    if (expression.length) {
      expression += ', ';
    }

    return expression + fragmentExprString;
  }

  _filterForArgs(astNode, modelClass, variables) {
    const args = astNode.arguments;

    if (args.length === 0) {
      return null;
    }

    const modelData = this.models[modelClass.tableName];
    const argObjects = new Array(args.length);

    for (let i = 0, l = args.length; i < l; i += 1) {
      const arg = args[i];
      const value = this._argValue(arg.value, variables);

      argObjects[i] = {
        name: arg.name.value,
        value,
      };
    }

    return (builder) => {
      for (let i = 0, l = argObjects.length; i < l; i += 1) {
        const arg = argObjects[i];

        if (!(typeof arg.value === 'undefined' && builder.internalOptions().skipUndefined)) {
          modelData.args[arg.name].query(builder, arg.value);
        }
      }
    };
  }

  _argValue(value, variables) {
    if (value.kind === KIND_VARIABLE) {
      return variables[value.name.value];
    } else if ('value' in value) {
      return value.value;
    } else if (Array.isArray(value.values)) {
      return value.values.map(curValue => this._argValue(curValue, variables));
    }
    throw new Error(`objection-graphql cannot handle argument value ${JSON.stringify(value)}`);
  }

  _filterForSelects(astNode, modelClass, astRoot) {
    if (!this.enableSelectFiltering) {
      return null;
    }

    const relations = modelClass.getRelations();
    const selects = this._collectSelects(astNode, relations, astRoot.fragments, []);

    if (selects.length === 0) {
      return null;
    }

    return (builder) => {
      const { jsonSchema } = modelClass;

      builder.select(selects.map((it) => {
        const col = modelClass.propertyNameToColumnName(it);

        if (jsonSchema.properties[it]) {
          return `${builder.tableRefFor(modelClass)}.${col}`;
        }
        return col;
      }));
    };
  }

  _collectSelects(astNode, relations, fragments, selects) {
    for (let i = 0, l = astNode.selectionSet.selections.length; i < l; i += 1) {
      const selectionNode = astNode.selectionSet.selections[i];

      if (selectionNode.kind === KIND_FRAGMENT_SPREAD) {
        this._collectSelects(fragments[selectionNode.name.value], relations, fragments, selects);
      } else {
        const relation = relations[selectionNode.name.value];
        const isMetaField = GRAPHQL_META_FIELDS.indexOf(selectionNode.name.value) !== -1;

        if (!relation && !isMetaField) {
          selects.push(selectionNode.name.value);
        }
      }
    }

    return selects;
  }
}

function fieldNameForModel(modelClass) {
  return _.camelCase(utils.typeNameForModel(modelClass));
}

function toJson(result) {
  if (_.isArray(result)) {
    for (let i = 0, l = result.length; i < l; i += 1) {
      result[i] = result[i].$toJson();
    }
  } else {
    result = result && result.$toJson();
  }

  return result;
}

module.exports = SchemaBuilder;
<commit_msg>`model` as a instance variable was already taken<commit_after>const _ = require('lodash');
const utils = require('./utils');
const objection = require('objection');
const graphqlRoot = require('graphql');

const jsonSchemaUtils = require('./jsonSchema');
const defaultArgFactories = require('./argFactories');

const { GraphQLObjectType, GraphQLSchema, GraphQLList } = graphqlRoot;

// Default arguments that are excluded from the relation arguments.
const OMIT_FROM_RELATION_ARGS = [
  // We cannot use `range` in the relation arguments since the relations are fetched
  // for multiple objects at a time. Limiting the result set would limit the combined
  // result, and not the individual model's relation.
  'range',
];

const GRAPHQL_META_FIELDS = [
  '__typename',
];

// GraphQL AST node types.
const KIND_FRAGMENT_SPREAD = 'FragmentSpread';
const KIND_VARIABLE = 'Variable';

class SchemaBuilder {
  constructor() {
    this.models = {};
    this.typeCache = {};
    this.filterIndex = 1;
    this.argFactories = [];
    this.enableSelectFiltering = true;
    this.defaultArgNameMap = {
      eq: 'Eq',
      gt: 'Gt',
      gte: 'Gte',
      lt: 'Lt',
      lte: 'Lte',
      like: 'Like',
      isNull: 'IsNull',
      likeNoCase: 'LikeNoCase',
      in: 'In',
      notIn: 'NotIn',
      orderBy: 'orderBy',
      orderByDesc: 'orderByDesc',
      range: 'range',
    };
  }

  model(modelClass, opt) {
    opt = opt || {};

    if (!modelClass.jsonSchema) {
      throw new Error('modelClass must have a jsonSchema');
    }

    this.models[modelClass.tableName] = {
      modelClass,
      fields: null,
      args: null,
      opt,
    };

    return this;
  }

  allModels(models) {
    models.forEach(model => this.model(model))
  }

  defaultArgNames(defaultArgNameMap) {
    this.defaultArgNameMap = Object.assign({}, this.defaultArgNameMap, defaultArgNameMap);
    return this;
  }

  argFactory(argFactory) {
    this.argFactories.push(argFactory);
    return this;
  }

  selectFiltering(enable) {
    this.enableSelectFiltering = !!enable;
    return this;
  }

  extendWithMutations(mutations) {
    if (!(mutations instanceof GraphQLObjectType || mutations instanceof Function)) {
      throw new TypeError('mutations should be a function or an object of type GraphQLObjectType');
    }

    this.mutation = mutations;
    return this;
  }

  setBuilderOptions(options) {
    this.builderOptions = options;
    return this;
  }

  build() {
    _.forOwn(this.models, (modelData) => {
      modelData.fields = jsonSchemaUtils.jsonSchemaToGraphQLFields(modelData.modelClass.jsonSchema, {
        include: modelData.opt.include,
        exclude: modelData.opt.exclude,
        typeNamePrefix: utils.typeNameForModel(modelData.modelClass),
        typeCache: this.typeCache,
      });

      modelData.args = this._argsForModel(modelData);
    });

    const schemaSetup = {
      query: new GraphQLObjectType({
        name: 'Query',
        fields: () => {
          const fields = {};

          _.forOwn(this.models, (modelData) => {
            const defaultFieldName = fieldNameForModel(modelData.modelClass);
            const singleFieldName = modelData.opt.fieldName || defaultFieldName;
            const listFieldName = modelData.opt.listFieldName || (`${defaultFieldName}s`);

            fields[singleFieldName] = this._rootSingleField(modelData);
            fields[listFieldName] = this._rootListField(modelData);
          });

          return fields;
        },
      }),
    };

    if (this.mutation) {
      if (this.mutation instanceof Function) {
        schemaSetup.mutation = this.mutation(this);
      } else {
        schemaSetup.mutation = this.mutation;
      }
    }

    return new GraphQLSchema(schemaSetup);
  }

  _argsForModel(modelData) {
    const factories = defaultArgFactories(this.defaultArgNameMap, { typeCache: this.typeCache }).concat(this.argFactories);

    return factories.reduce((args, factory) => Object.assign(args, factory(modelData.fields, modelData.modelClass)), {});
  }

  _rootSingleField(modelData) {
    return {
      type: this._typeForModel(modelData),
      args: modelData.args,
      resolve: this._resolverForModel(modelData, (query) => {
        query.first();
      }),
    };
  }

  _rootListField(modelData) {
    return {
      type: new GraphQLList(this._typeForModel(modelData)),
      args: modelData.args,
      resolve: this._resolverForModel(modelData),
    };
  }

  _typeForModel(modelData) {
    const typeName = utils.typeNameForModel(modelData.modelClass);

    if (!this.typeCache[typeName]) {
      this.typeCache[typeName] = new GraphQLObjectType({
        name: typeName,
        fields: () => Object.assign({}, this._attrFields(modelData), this._relationFields(modelData)),
      });
    }

    return this.typeCache[typeName];
  }

  _attrFields(modelData) {
    return modelData.fields;
  }

  _relationFields(modelData) {
    const fields = {};

    _.forOwn(modelData.modelClass.getRelations(), (relation) => {
      const relationModel = this.models[relation.relatedModelClass.tableName];

      if (!relationModel) {
        // If the relation model has not been given for the builder using `model()` method
        // we don't handle the relations that have that class.
        return;
      }

      if (utils.isExcluded(relationModel.opt, relation.name)) {
        // If the property by the relation's name has been excluded, skip this relation.
        return;
      }

      fields[relation.name] = this._relationField(relationModel, relation);
    });

    return fields;
  }

  _relationField(modelData, relation) {
    if (relation instanceof objection.HasOneRelation
        || relation instanceof objection.BelongsToOneRelation
        || relation instanceof objection.HasOneThroughRelation) {
      return {
        type: this._typeForModel(modelData),
        args: _.omit(modelData.args, OMIT_FROM_RELATION_ARGS),
      };
    } else if (relation instanceof objection.HasManyRelation || relation instanceof objection.ManyToManyRelation) {
      return {
        type: new GraphQLList(this._typeForModel(modelData)),
        args: _.omit(modelData.args, OMIT_FROM_RELATION_ARGS),
      };
    }
    throw new Error(`relation type "${relation.constructor.name}" is not supported`);
  }

  _resolverForModel(modelData, extraQuery) {
    return (ctx, ignore1, ignore2, data) => {
      ctx = ctx || {};

      const { modelClass } = modelData;
      const ast = (data.fieldASTs || data.fieldNodes)[0];
      const eager = this._buildEager(ast, modelClass, data);
      const argFilter = this._filterForArgs(ast, modelClass, data.variableValues);
      const selectFilter = this._filterForSelects(ast, modelClass, data);
      const builder = modelClass.query(ctx.knex);

      if (this.builderOptions && this.builderOptions.skipUndefined) {
        builder.skipUndefined();
      }

      if (ctx.onQuery) {
        ctx.onQuery(builder, ctx);
      }

      if (argFilter) {
        builder.modify(argFilter);
      }

      if (selectFilter) {
        builder.modify(selectFilter);
      }

      if (extraQuery) {
        builder.modify(extraQuery);
      }

      if (eager.expression) {
        builder.eager(eager.expression, eager.filters);
      }

      return builder.then(toJson);
    };
  }

  _buildEager(astNode, modelClass, astRoot) {
    const eagerExpr = this._buildEagerSegment(astNode, modelClass, astRoot);

    if (eagerExpr.expression.length) {
      eagerExpr.expression = `[${eagerExpr.expression}]`;
    }

    return eagerExpr;
  }

  _buildEagerSegment(astNode, modelClass, astRoot) {
    const filters = {};
    const relations = modelClass.getRelations();
    let expression = '';

    for (let i = 0, l = astNode.selectionSet.selections.length; i < l; i += 1) {
      const selectionNode = astNode.selectionSet.selections[i];
      const relation = relations[selectionNode.name.value];

      if (relation) {
        expression = this._buildEagerRelationSegment(selectionNode, relation, expression, filters, astRoot);
      } else if (selectionNode.kind === KIND_FRAGMENT_SPREAD) {
        expression = this._buildEagerFragmentSegment(selectionNode, modelClass, expression, filters, astRoot);
      }
    }

    return {
      expression,
      filters,
    };
  }

  _buildEagerRelationSegment(selectionNode, relation, expression, filters, astRoot) {
    let relExpr = selectionNode.name.value;

    const selectFilter = this._filterForSelects(selectionNode, relation.relatedModelClass, astRoot);
    const filterNames = [];

    if (selectFilter) {
      this.filterIndex += 1;
      const filterName = `s${this.filterIndex}`;

      filterNames.push(filterName);
      filters[filterName] = selectFilter;
    }

    if (selectionNode.arguments.length) {
      const argFilter = this._filterForArgs(selectionNode, relation.relatedModelClass, astRoot.variableValues);

      if (argFilter) {
        this.filterIndex += 1;
        const filterName = `f${this.filterIndex}`;

        filterNames.push(filterName);
        filters[filterName] = argFilter;
      }
    }

    if (filterNames.length) {
      relExpr += `(${filterNames.join(', ')})`;
    }

    const subExpr = this._buildEager(selectionNode, relation.relatedModelClass, astRoot);

    if (subExpr.expression.length) {
      relExpr += `.${subExpr.expression}`;
      Object.assign(filters, subExpr.filters);
    }

    if (expression.length) {
      expression += ', ';
    }

    return expression + relExpr;
  }

  _buildEagerFragmentSegment(selectionNode, modelClass, expression, filters, astRoot) {
    const fragmentSelection = astRoot.fragments[selectionNode.name.value];
    const fragmentExpr = this._buildEagerSegment(fragmentSelection, modelClass, astRoot);
    let fragmentExprString = '';

    if (fragmentExpr.expression.length) {
      fragmentExprString += fragmentExpr.expression;
      Object.assign(filters, fragmentExpr.filters);
    }

    if (expression.length) {
      expression += ', ';
    }

    return expression + fragmentExprString;
  }

  _filterForArgs(astNode, modelClass, variables) {
    const args = astNode.arguments;

    if (args.length === 0) {
      return null;
    }

    const modelData = this.models[modelClass.tableName];
    const argObjects = new Array(args.length);

    for (let i = 0, l = args.length; i < l; i += 1) {
      const arg = args[i];
      const value = this._argValue(arg.value, variables);

      argObjects[i] = {
        name: arg.name.value,
        value,
      };
    }

    return (builder) => {
      for (let i = 0, l = argObjects.length; i < l; i += 1) {
        const arg = argObjects[i];

        if (!(typeof arg.value === 'undefined' && builder.internalOptions().skipUndefined)) {
          modelData.args[arg.name].query(builder, arg.value);
        }
      }
    };
  }

  _argValue(value, variables) {
    if (value.kind === KIND_VARIABLE) {
      return variables[value.name.value];
    } else if ('value' in value) {
      return value.value;
    } else if (Array.isArray(value.values)) {
      return value.values.map(curValue => this._argValue(curValue, variables));
    }
    throw new Error(`objection-graphql cannot handle argument value ${JSON.stringify(value)}`);
  }

  _filterForSelects(astNode, modelClass, astRoot) {
    if (!this.enableSelectFiltering) {
      return null;
    }

    const relations = modelClass.getRelations();
    const selects = this._collectSelects(astNode, relations, astRoot.fragments, []);

    if (selects.length === 0) {
      return null;
    }

    return (builder) => {
      const { jsonSchema } = modelClass;

      builder.select(selects.map((it) => {
        const col = modelClass.propertyNameToColumnName(it);

        if (jsonSchema.properties[it]) {
          return `${builder.tableRefFor(modelClass)}.${col}`;
        }
        return col;
      }));
    };
  }

  _collectSelects(astNode, relations, fragments, selects) {
    for (let i = 0, l = astNode.selectionSet.selections.length; i < l; i += 1) {
      const selectionNode = astNode.selectionSet.selections[i];

      if (selectionNode.kind === KIND_FRAGMENT_SPREAD) {
        this._collectSelects(fragments[selectionNode.name.value], relations, fragments, selects);
      } else {
        const relation = relations[selectionNode.name.value];
        const isMetaField = GRAPHQL_META_FIELDS.indexOf(selectionNode.name.value) !== -1;

        if (!relation && !isMetaField) {
          selects.push(selectionNode.name.value);
        }
      }
    }

    return selects;
  }
}

function fieldNameForModel(modelClass) {
  return _.camelCase(utils.typeNameForModel(modelClass));
}

function toJson(result) {
  if (_.isArray(result)) {
    for (let i = 0, l = result.length; i < l; i += 1) {
      result[i] = result[i].$toJson();
    }
  } else {
    result = result && result.$toJson();
  }

  return result;
}

module.exports = SchemaBuilder;
<|endoftext|>