<commit_before>var assert = require('assert');
var step = require('step');

var util = require('util');
var BaseController = require('./base');

var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');

var DataviewBackend = require('../backends/dataview');

var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');

var QueryTables = require('cartodb-query-tables');

/**
 * @param {AuthApi} authApi
 * @param {PgConnection} pgConnection
 * @param {MapStore} mapStore
 * @param {TileBackend} tileBackend
 * @param {PreviewBackend} previewBackend
 * @param {AttributesBackend} attributesBackend
 * @param {WidgetBackend} widgetBackend
 * @param {SurrogateKeysCache} surrogateKeysCache
 * @param {UserLimitsApi} userLimitsApi
 * @param {LayergroupAffectedTables} layergroupAffectedTables
 * @constructor
 */
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
                          widgetBackend, surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
    BaseController.call(this, authApi, pgConnection);

    this.pgConnection = pgConnection;
    this.mapStore = mapStore;
    this.tileBackend = tileBackend;
    this.previewBackend = previewBackend;
    this.attributesBackend = attributesBackend;
    this.widgetBackend = widgetBackend;
    this.surrogateKeysCache = surrogateKeysCache;
    this.userLimitsApi = userLimitsApi;
    this.layergroupAffectedTables = layergroupAffectedTables;

    this.dataviewBackend = new DataviewBackend();
}

util.inherits(LayergroupController, BaseController);

module.exports = LayergroupController;

LayergroupController.prototype.register = function(app) {
    app.get(app.base_url_mapconfig +
        '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
        this.tile.bind(this));

    app.get(app.base_url_mapconfig +
        '/:token/:z/:x/:y.:format', cors(), userMiddleware,
        this.tile.bind(this));

    app.get(app.base_url_mapconfig +
        '/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
        this.layer.bind(this));

    app.get(app.base_url_mapconfig +
        '/:token/:layer/attributes/:fid', cors(), userMiddleware,
        this.attributes.bind(this));

    app.get(app.base_url_mapconfig +
        '/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), userMiddleware,
        this.center.bind(this));

    app.get(app.base_url_mapconfig +
        '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), userMiddleware,
        this.bbox.bind(this));

    // Undocumented/non-supported API endpoint methods.
    // Use at your own peril.
    app.get(app.base_url_mapconfig +
        '/:token/:layer/widget/:widgetName', cors(), userMiddleware,
        this.widget.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
        this.widgetSearch.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/dataview/:dataviewName', cors(), userMiddleware,
        this.dataview.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/dataview/:dataviewName/search', cors(), userMiddleware,
        this.dataviewSearch.bind(this));
};

LayergroupController.prototype.dataview = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveDataview(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET DATAVIEW');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.dataviewSearch = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function searchDataview(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.widget = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveList(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.widgetBackend.getWidget(mapConfigProvider, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET WIDGET');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.widgetSearch = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveList(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.widgetBackend.search(mapConfigProvider, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET WIDGET');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.attributes = function(req, res) {
    var self = this;

    req.profiler.start('windshaft.maplayer_attribute');

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveFeatureAttributes(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET ATTRIBUTES');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
LayergroupController.prototype.tile = function(req, res) {
    req.profiler.start('windshaft.map_tile');
    this.tileOrLayer(req, res);
};

// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
LayergroupController.prototype.layer = function(req, res, next) {
    if (req.params.token === 'static') {
        return next();
    }
    req.profiler.start('windshaft.maplayer_tile');
    this.tileOrLayer(req, res);
};

LayergroupController.prototype.tileOrLayer = function (req, res) {
    var self = this;

    step(
        function mapController$prepareParams() {
            self.req2params(req, this);
        },
        function mapController$getTileOrGrid(err) {
            assert.ifError(err);
            self.tileBackend.getTile(
                new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
                req.params, this
            );
        },
        function mapController$finalize(err, tile, headers, stats) {
            req.profiler.add(stats);
            self.finalizeGetTileOrGrid(err, req, res, tile, headers);
        }
    );
};

// This function is meant for being called as the very last
// step by all endpoints serving tiles or grids
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
    var supportedFormats = {
        grid_json: true,
        json_torque: true,
        torque_json: true,
        png: true
    };

    var formatStat = 'invalid';
    if (req.params.format) {
        var format = req.params.format.replace('.', '_');
        if (supportedFormats[format]) {
            formatStat = format;
        }
    }

    if (err) {
        // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
        var errMsg = err.message ? ( '' + err.message ) : ( '' + err );

        // Rewrite mapnik parsing errors to start with layer number
        var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
        if (matches) {
            errMsg = 'style'+matches[2]+': ' + matches[1];
        }
        err.message = errMsg;

        this.sendError(req, res, err, 'TILE RENDER');
        global.statsClient.increment('windshaft.tiles.error');
        global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
    } else {
        this.sendResponse(req, res, tile, 200, headers);
        global.statsClient.increment('windshaft.tiles.success');
        global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
    }
};

LayergroupController.prototype.bbox = function(req, res) {
    this.staticMap(req, res, +req.params.width, +req.params.height, {
        west: +req.params.west,
        north: +req.params.north,
        east: +req.params.east,
        south: +req.params.south
    });
};

LayergroupController.prototype.center = function(req, res) {
    this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
        lng: +req.params.lng,
        lat: +req.params.lat
    });
};

LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
    var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
    req.params.layer = 'all';
    req.params.format = 'png';

    var self = this;

    step(
        function reqParams() {
            self.req2params(req, this);
        },
        function getImage(err) {
            assert.ifError(err);
            if (center) {
                self.previewBackend.getImage(
                    new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
                    format, width, height, zoom, center, this);
            } else {
                self.previewBackend.getImage(
                    new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
                    format, width, height, zoom /* bounds */, this);
            }
        },
        function handleImage(err, image, headers, stats) {
            req.profiler.done('render-' + format);
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'STATIC_MAP');
            } else {
                res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
                self.sendResponse(req, res, image, 200);
            }
        }
    );
};

LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
    var self = this;

    res.set('Cache-Control', 'public,max-age=31536000');

    // Set Last-Modified header
    var lastUpdated;
    if (req.params.cache_buster) {
        // Assuming cache_buster is a timestamp
        lastUpdated = new Date(parseInt(req.params.cache_buster));
    } else {
        lastUpdated = new Date();
    }
    res.set('Last-Modified', lastUpdated.toUTCString());

    var dbName = req.params.dbname;
    step(
        function getAffectedTables() {
            self.getAffectedTables(req.context.user, dbName, req.params.token, this);
        },
        function sendResponse(err, affectedTables) {
            req.profiler.done('affectedTables');
            if (err) {
                global.logger.warn('ERROR generating cache channel: ' + err);
            }
            if (!!affectedTables) {
                res.set('X-Cache-Channel', affectedTables.getCacheChannel());
                self.surrogateKeysCache.tag(res, affectedTables);
            }
            self.send(req, res, body, status, headers);
        }
    );

};

LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {

    if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
        return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
    }

    var self = this;
    step(
        function extractSQL() {
            step(
                function loadFromStore() {
                    self.mapStore.load(layergroupId, this);
                },
                function getSQL(err, mapConfig) {
                    assert.ifError(err);

                    var queries = mapConfig.getLayers()
                        .map(function(lyr) {
                            return lyr.options.sql;
                        })
                        .filter(function(sql) {
                            return !!sql;
                        });

                    return queries.length ? queries.join(';') : null;
                },
                this
            );
        },
        function findAffectedTables(err, sql) {
            assert.ifError(err);

            if ( ! sql ) {
                throw new Error("this request doesn't need an X-Cache-Channel generated");
            }

            step(
                function getConnection() {
                    self.pgConnection.getConnection(user, this);
                },
                function getAffectedTables(err, connection) {
                    assert.ifError(err);

                    QueryTables.getAffectedTablesFromQuery(connection, sql, this);
                },
                this
            );
        },
        function buildCacheChannel(err, tables) {
            assert.ifError(err);
            self.layergroupAffectedTables.set(dbName, layergroupId, tables);

            return tables;
        },
        callback
    );
};
<commit_msg>Add fake analysis node status endpoint<commit_after>var assert = require('assert');
var step = require('step');

var util = require('util');
var BaseController = require('./base');

var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');

var DataviewBackend = require('../backends/dataview');

var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');

var QueryTables = require('cartodb-query-tables');

/**
 * @param {AuthApi} authApi
 * @param {PgConnection} pgConnection
 * @param {MapStore} mapStore
 * @param {TileBackend} tileBackend
 * @param {PreviewBackend} previewBackend
 * @param {AttributesBackend} attributesBackend
 * @param {WidgetBackend} widgetBackend
 * @param {SurrogateKeysCache} surrogateKeysCache
 * @param {UserLimitsApi} userLimitsApi
 * @param {LayergroupAffectedTables} layergroupAffectedTables
 * @constructor
 */
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
                          widgetBackend, surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
    BaseController.call(this, authApi, pgConnection);

    this.pgConnection = pgConnection;
    this.mapStore = mapStore;
    this.tileBackend = tileBackend;
    this.previewBackend = previewBackend;
    this.attributesBackend = attributesBackend;
    this.widgetBackend = widgetBackend;
    this.surrogateKeysCache = surrogateKeysCache;
    this.userLimitsApi = userLimitsApi;
    this.layergroupAffectedTables = layergroupAffectedTables;

    this.dataviewBackend = new DataviewBackend();
}

util.inherits(LayergroupController, BaseController);

module.exports = LayergroupController;

LayergroupController.prototype.register = function(app) {
    app.get(app.base_url_mapconfig +
        '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
        this.tile.bind(this));

    app.get(app.base_url_mapconfig +
        '/:token/:z/:x/:y.:format', cors(), userMiddleware,
        this.tile.bind(this));

    app.get(app.base_url_mapconfig +
        '/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
        this.layer.bind(this));

    app.get(app.base_url_mapconfig +
        '/:token/:layer/attributes/:fid', cors(), userMiddleware,
        this.attributes.bind(this));

    app.get(app.base_url_mapconfig +
        '/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), userMiddleware,
        this.center.bind(this));

    app.get(app.base_url_mapconfig +
        '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), userMiddleware,
        this.bbox.bind(this));

    // Undocumented/non-supported API endpoint methods.
    // Use at your own peril.
    app.get(app.base_url_mapconfig +
        '/:token/:layer/widget/:widgetName', cors(), userMiddleware,
        this.widget.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
        this.widgetSearch.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/dataview/:dataviewName', cors(), userMiddleware,
        this.dataview.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/dataview/:dataviewName/search', cors(), userMiddleware,
        this.dataviewSearch.bind(this));

    app.get(app.base_url_mapconfig +
            '/:token/analysis/:analysisIndex/node/:nodeId', cors(), userMiddleware,
        this.analysisNodeStatus.bind(this));
};

LayergroupController.prototype.analysisNodeStatus = function(req, res) {
    this.sendResponse(req, res, {
        node_id: req.params.nodeId,
        status: 'ready'
    }, 200);
};

LayergroupController.prototype.dataview = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveDataview(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET DATAVIEW');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.dataviewSearch = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function searchDataview(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.widget = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveList(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.widgetBackend.getWidget(mapConfigProvider, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET WIDGET');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.widgetSearch = function(req, res) {
    var self = this;

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveList(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.widgetBackend.search(mapConfigProvider, req.params, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET WIDGET');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

LayergroupController.prototype.attributes = function(req, res) {
    var self = this;

    req.profiler.start('windshaft.maplayer_attribute');

    step(
        function setupParams() {
            self.req2params(req, this);
        },
        function retrieveFeatureAttributes(err) {
            assert.ifError(err);

            var mapConfigProvider = new MapStoreMapConfigProvider(
                self.mapStore, req.context.user, self.userLimitsApi, req.params
            );
            self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
        },
        function finish(err, tile, stats) {
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'GET ATTRIBUTES');
            } else {
                self.sendResponse(req, res, tile, 200);
            }
        }
    );

};

// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
LayergroupController.prototype.tile = function(req, res) {
    req.profiler.start('windshaft.map_tile');
    this.tileOrLayer(req, res);
};

// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
LayergroupController.prototype.layer = function(req, res, next) {
    if (req.params.token === 'static') {
        return next();
    }
    req.profiler.start('windshaft.maplayer_tile');
    this.tileOrLayer(req, res);
};

LayergroupController.prototype.tileOrLayer = function (req, res) {
    var self = this;

    step(
        function mapController$prepareParams() {
            self.req2params(req, this);
        },
        function mapController$getTileOrGrid(err) {
            assert.ifError(err);
            self.tileBackend.getTile(
                new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
                req.params, this
            );
        },
        function mapController$finalize(err, tile, headers, stats) {
            req.profiler.add(stats);
            self.finalizeGetTileOrGrid(err, req, res, tile, headers);
        }
    );
};

// This function is meant for being called as the very last
// step by all endpoints serving tiles or grids
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
    var supportedFormats = {
        grid_json: true,
        json_torque: true,
        torque_json: true,
        png: true
    };

    var formatStat = 'invalid';
    if (req.params.format) {
        var format = req.params.format.replace('.', '_');
        if (supportedFormats[format]) {
            formatStat = format;
        }
    }

    if (err) {
        // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
        var errMsg = err.message ? ( '' + err.message ) : ( '' + err );

        // Rewrite mapnik parsing errors to start with layer number
        var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
        if (matches) {
            errMsg = 'style'+matches[2]+': ' + matches[1];
        }
        err.message = errMsg;

        this.sendError(req, res, err, 'TILE RENDER');
        global.statsClient.increment('windshaft.tiles.error');
        global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
    } else {
        this.sendResponse(req, res, tile, 200, headers);
        global.statsClient.increment('windshaft.tiles.success');
        global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
    }
};

LayergroupController.prototype.bbox = function(req, res) {
    this.staticMap(req, res, +req.params.width, +req.params.height, {
        west: +req.params.west,
        north: +req.params.north,
        east: +req.params.east,
        south: +req.params.south
    });
};

LayergroupController.prototype.center = function(req, res) {
    this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
        lng: +req.params.lng,
        lat: +req.params.lat
    });
};

LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
    var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
    req.params.layer = 'all';
    req.params.format = 'png';

    var self = this;

    step(
        function reqParams() {
            self.req2params(req, this);
        },
        function getImage(err) {
            assert.ifError(err);
            if (center) {
                self.previewBackend.getImage(
                    new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
                    format, width, height, zoom, center, this);
            } else {
                self.previewBackend.getImage(
                    new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
                    format, width, height, zoom /* bounds */, this);
            }
        },
        function handleImage(err, image, headers, stats) {
            req.profiler.done('render-' + format);
            req.profiler.add(stats || {});

            if (err) {
                self.sendError(req, res, err, 'STATIC_MAP');
            } else {
                res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
                self.sendResponse(req, res, image, 200);
            }
        }
    );
};

LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
    var self = this;

    res.set('Cache-Control', 'public,max-age=31536000');

    // Set Last-Modified header
    var lastUpdated;
    if (req.params.cache_buster) {
        // Assuming cache_buster is a timestamp
        lastUpdated = new Date(parseInt(req.params.cache_buster));
    } else {
        lastUpdated = new Date();
    }
    res.set('Last-Modified', lastUpdated.toUTCString());

    var dbName = req.params.dbname;
    step(
        function getAffectedTables() {
            self.getAffectedTables(req.context.user, dbName, req.params.token, this);
        },
        function sendResponse(err, affectedTables) {
            req.profiler.done('affectedTables');
            if (err) {
                global.logger.warn('ERROR generating cache channel: ' + err);
            }
            if (!!affectedTables) {
                res.set('X-Cache-Channel', affectedTables.getCacheChannel());
                self.surrogateKeysCache.tag(res, affectedTables);
            }
            self.send(req, res, body, status, headers);
        }
    );

};

LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {

    if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
        return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
    }

    var self = this;
    step(
        function extractSQL() {
            step(
                function loadFromStore() {
                    self.mapStore.load(layergroupId, this);
                },
                function getSQL(err, mapConfig) {
                    assert.ifError(err);

                    var queries = mapConfig.getLayers()
                        .map(function(lyr) {
                            return lyr.options.sql;
                        })
                        .filter(function(sql) {
                            return !!sql;
                        });

                    return queries.length ? queries.join(';') : null;
                },
                this
            );
        },
        function findAffectedTables(err, sql) {
            assert.ifError(err);

            if ( ! sql ) {
                throw new Error("this request doesn't need an X-Cache-Channel generated");
            }

            step(
                function getConnection() {
                    self.pgConnection.getConnection(user, this);
                },
                function getAffectedTables(err, connection) {
                    assert.ifError(err);

                    QueryTables.getAffectedTablesFromQuery(connection, sql, this);
                },
                this
            );
        },
        function buildCacheChannel(err, tables) {
            assert.ifError(err);
            self.layergroupAffectedTables.set(dbName, layergroupId, tables);

            return tables;
        },
        callback
    );
};
<|endoftext|>


<commit_before>/* eslint-disable no-param-reassign, no-unused-expressions */

/**
 * Simple format function borrowed from PureMask.js
 * {@link https://github.com/romulobrasil/PureMask.js}
 *
 * @param {String} text String to mask (input value)
 * @param {String} [wholeMask] Mask format, like `####-##`
 * @returns {string} Formatted text
 */
export default function (text, wholeMask) {
  if (!wholeMask) return text;

  const maskStartRegExp = /^([^#ANX]+)/;

  if (+text.length === 1 && maskStartRegExp.test(wholeMask)) {
    text = maskStartRegExp.exec(wholeMask)[0] + text;
  }

  let newText = '';
  let charOffset = 0;

  // Cleans data to  avoid value loss on dynamic mask changing
  for (let maskIndex = 0; maskIndex < wholeMask.length; maskIndex += 1) {
    const mask = wholeMask.charAt(maskIndex);
    switch (mask) {
      case '#':
        break;
      case 'A':
        break;
      case '?':
        break;
      case 'N':
        break;
      case 'X':
        break;
      default:
        text = text.replace(mask, '');
    }
  }
  for (let maskIndex = 0, x = 1; x && maskIndex < wholeMask.length; maskIndex += 1) {
    const char = text.charAt(maskIndex - charOffset);
    const mask = wholeMask.charAt(maskIndex);

    switch (mask) {
      case '#':
        /\d/.test(char) ? newText += char : x = 0;
        break;
      case 'A':
        /[a-z]/i.test(char) ? newText += char : x = 0;
        break;
      case 'N':
        /[a-z0-9]/i.test(char) ? newText += char : x = 0;
        break;
      // Skips testing if optional field is specified
      case '?':
        charOffset += 1;
        break;
      case 'X':
        newText += char;
        break;
      default:
        newText += mask;

        // preserve characters that are in the same spot we need to insert a mask
        // character by shifting the data over to the right (issue #5, & #7)
        if (char && char !== mask) {
          text = ` ${text}`;
        }

        break;
    }
  }
  return newText;
}
<commit_msg>feat(format): ditch `pureMask` and use `text-mask-core` instead<commit_after>import conformToMask from 'text-mask-core/src/conformToMask';

const NEXT_CHAR_OPTIONAL = {
  __nextCharOptional__: true,
};

/**
 * @param {String} text String to mask (input value)
 * @param {String} [wholeMask] Mask format, like `####-##`
 * @returns {string} Formatted text
 */
export default function (text, wholeMask) {
  if (!wholeMask) return text;
  const replacementMap = {
    '#': /\d/,
    A: /[a-z]/i,
    N: /[a-z0-9]/i,
    '?': NEXT_CHAR_OPTIONAL,
    X: /./,
  };

  const stringToRegexp = (str) => {
    const lastSlash = str.lastIndexOf('/');
    return new RegExp(
      str.slice(1, lastSlash),
      str.slice(lastSlash + 1),
    );
  };
  const makeRegexpOptional = charRegexp => (
    stringToRegexp(
      charRegexp.toString()
        .replace(
          /.(\/)[gmiyus]{0,6}$/,
          match => match.replace('/', '?/'),
        ),
    )
  )

  const escapeIfNeeded = char => ('[\\^$.|?*+()'.split('').includes(char) ? `\\${char}` : char);
  const charRegexp = char => new RegExp(`/[${escapeIfNeeded(char)}]/`);
  const isRegexp = entity => entity instanceof RegExp;
  const castToRegexp = char => (isRegexp(char) ? char : charRegexp(char));

  const generatedMask = wholeMask
    .split('')
    .map((char, index, array) => {
      const maskChar = replacementMap[char] || char;
      const previousChar = array[index - 1];
      const previousMaskChar = replacementMap[previousChar] || previousChar;
      if (maskChar === NEXT_CHAR_OPTIONAL) {
        return null;
      }
      if (previousMaskChar === NEXT_CHAR_OPTIONAL) {
        const casted = castToRegexp(maskChar);
        const optionalRegexp = makeRegexpOptional(casted);
        return optionalRegexp;
      }
      return maskChar;
    })
    .filter(Boolean);

  const { conformedValue } = conformToMask(text, generatedMask, { guide: false });
  return conformedValue;
}
<|endoftext|>


<commit_before>/*!
 * Visual Novel JS
 * version: 1.0-2014.06.15
 * Copyright (c) 2014 Yuki selcher123@gmail.com
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

( function( w ) {

	/**
	 * Class: VisualNovel
	 *
	 * @param id = id of visual novel div
	 *             used as reference for novel instance
	 * @param width = width
	 * @param height = height
	 * @param imgPath = path to image folder
	 */
	function VisualNovel( id, width, height, imgPath ) {

		if ( this instanceof VisualNovel ) {

			this.novelId = id;
			this.novelTitle = "";
			this.novelSubtitle = "";
			this.novelMode = "dialog"; // dialog or novel

			// store images for preloading, or let user handle it?
			// preloading not yet implemented ( use preloadjs )
			this.imgPath = imgPath ? imgPath : "";
			this.images = [];

			// div elements
			this.novelContainerId = null;

			// TODO: place default values here...
			this.defaultVal = {};

			// screen
			this.screenWidth = width;
			this.screenHeight = height;

			// scene
			this.sceneFloorHeight = this.sceneFloorWidth =
				height > width ? height : width;

			return this;

		} else {

			return new VisualNovel( width, height, imgPath );

		}

	}

	/**
	 * Variable: modules
	 *
	 * Holds the modules.
	 * Each module has an init and reset method.
	 *
	 * TODO: implement each module using browserify / webpack
	 */
	VisualNovel.prototype.modules = [];

	/**
	 * Function: init
	 *
	 * Initialize visualize novel
	 * when creating a new VisualNovel instance.
	 *
	 * @param callback = function to perform after initialization
	 *					 ( e.g. add effects to start screen )
	 */
	VisualNovel.prototype.init = function init( callback ) {

		var novelId = this.novelId;

		this.initNovelContainer( novelId );

		this.initModules( novelId );

		if ( callback ) {

			setTimeout( callback );

		}

	};

	/**
	 * Function: reset
	 *
	 * Reset novel to return to the start screen.
	 */
	VisualNovel.prototype.reset = function reset() {

		function eventToAdd() {

			this.resetModules();
			this.showStartScreen( true );

		}

		this.eventTracker.addEvent( "wait", eventToAdd.bind( this ) );

	};

	/**
	 * Function: initModules
	 *
	 * Call init method of modules.
	 *
	 * @param novelId = id of visual novel div, and instance reference
	 */
	VisualNovel.prototype.initModules = function initModules( novelId ) {

		var modules = this.modules;

		for ( var i = modules.length; i--; ) {

			modules[ i ].init.call( this, novelId );

		}

	};

	/**
	 * Function: resetModules
	 *
	 * Call reset method of modules.
	 */
	VisualNovel.prototype.resetModules = function resetModules() {

		var modules = this.modules;

		for ( var i = modules.length; i--; ) {

			modules[ i ].reset.call( this );

		}

	};

	/**
	 * Function: initNovelContainer
	 *
	 * Initialize novel container template, size, and references.
	 *
	 * @param novelId = id of visual novel div, and instance reference
	 *
	 */
	VisualNovel.prototype.initNovelContainer = function initNovelContainer( novelId ) {

		this.novelContainerId = document.getElementById( novelId );

		var content = this.buildNovelContainerContent( novelId );

		this.setNovelContainerContent( content );
		this.setNovelContainerSize( this.screenWidth, this.screenHeight );

	};

	/**
	 * Function: attachToNovelContainer
	 *
	 * Attach template of new element to the novel container
	 *
	 * @param template = template of element to be attached
	 */
	VisualNovel.prototype.attachToNovelContainer = function attachToNovelContainer( template ) {

		var div = document.createElement( "div" );

		div.innerHTML = template;

		var attachedElement = div.firstChild;

		this.novelContainerId.firstChild.appendChild( attachedElement );

		return attachedElement;

	};

	/**
	 * Variable: novelContainerTemplate
	 *
	 * Template for novel container.
	 *
	 * TODO: let each module add its own container to the main container
	 */
	VisualNovel.prototype.novelContainerTemplate = [
		"
",
			"

",
			"

",
			"

",
			"

",
			"

",
			"

",
			"

",
			"

",
		"
"
	].join( "" );

	/**
	 * Function: buildNovelContainer
	 *
	 * Build the html content for the novel container.
	 *
	 * @param novelId = id of visual novel div, and instance reference
	 */
	VisualNovel.prototype.buildNovelContainerContent = function buildNovelContainerContent( novelId ) {

		var content = "";

		if ( this.parser ) {

			content = this.parser.parseTemplate( this.novelContainerTemplate, { "novelId": novelId } );

		} else {

			this.log( "[core]VisualNovelJS Error: parser module not found" );

		}

		return content;

	};

	/**
	 * Function: setNovelContainerContent
	 *
	 * Set the html content of the novel container.
	 *
	 * @param content = content of the novel container
	 */
	VisualNovel.prototype.setNovelContainerContent = function setNovelContainerContent( content ) {

		this.novelContainerId.innerHTML = content;

	};

	/**
	 * Function: setNovelContainerSize
	 *
	 * Set the size of the main novel container, and the containers inside.
	 *
	 * @param width = new width
	 * @param height = new height
	 */
	VisualNovel.prototype.setNovelContainerSize = function setNovelContainerSize( width, height ) {

		// TODO : may need refactoring

		// novel container
		var novelContainer = this.novelContainerId;
		var sizeStyle = ";width:" + width + "px;height:" + height + "px;";
		var newStyle = ";overflow:hidden" + sizeStyle;

		novelContainer.style.cssText += newStyle;

		// containers ( scene, character, dialog, menu, ... )
		var containers = novelContainer.getElementsByClassName( "novel" );

		for ( var i = containers.length; i--; ) {

			containers[ i ].style.cssText += sizeStyle;

		}

	};

	/**
	 * Function: log
	 *
	 * Log error message.
	 *
	 * @param msg = error message
	 */
	VisualNovel.prototype.log = function log( msg ) {

		if ( console && console.log ) {

			console.log( msg );

		}

	};

	// Add as global namespace
	w.VisualNovel = VisualNovel;

} )( window );<commit_msg>Update core module<commit_after>/*!
 * Visual Novel JS
 * version: 1.0-2014.06.15
 * Copyright (c) 2014 Yuki selcher123@gmail.com
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

( function( w ) {

	/**
	 * Class: VisualNovel
	 *
	 * @param id = id of visual novel div
	 *             used as reference for novel instance
	 * @param width = width
	 * @param height = height
	 * @param imgPath = path to image folder
	 */
	function VisualNovel( id, width, height, imgPath ) {

		if ( this instanceof VisualNovel ) {

			this.novelId = id;

			// dialog / novel
			this.novelMode = "dialog";

			// store images for preloading, or let user handle it?
			// preloading not yet implemented ( use preloadjs )
			this.imgPath = imgPath ? imgPath : "";
			this.images = [];

			// div elements
			this.novelContainerId = null;

			// TODO: place default values here...
			this.defaultVal = {};

			// screen
			this.screenWidth = width;
			this.screenHeight = height;

			// scene
			this.sceneFloorHeight = this.sceneFloorWidth =
				height > width ? height : width;

			return this;

		} else {

			return new VisualNovel( width, height, imgPath );

		}

	}

	/**
	 * Variable: modules
	 *
	 * Holds the modules.
	 * Each module has an init and reset method.
	 *
	 * TODO: implement each module using browserify / webpack
	 */
	VisualNovel.prototype.modules = [];

	/**
	 * Function: init
	 *
	 * Initialize visualize novel
	 * when creating a new VisualNovel instance.
	 *
	 * @param callback = function to perform after initialization
	 *					 ( e.g. add effects to start screen )
	 */
	VisualNovel.prototype.init = function init( callback ) {

		var novelId = this.novelId;

		this.initNovelContainer( novelId );

		this.initModules( novelId );

		if ( callback ) {

			setTimeout( callback );

		}

	};

	/**
	 * Function: reset
	 *
	 * Reset novel to return to the start screen.
	 */
	VisualNovel.prototype.reset = function reset() {

		function eventToAdd() {

			this.resetModules();
			this.showStartScreen( true );

		}

		this.createEvent( "wait", eventToAdd.bind( this ) );

	};

	/**
	 * Function: initModules
	 *
	 * Call init method of modules.
	 *
	 * @param novelId = id of visual novel div, and instance reference
	 */
	VisualNovel.prototype.initModules = function initModules( novelId ) {

		var modules = this.modules;

		for ( var i = modules.length; i--; ) {

			modules[ i ].init.call( this, novelId );

		}

	};

	/**
	 * Function: resetModules
	 *
	 * Call reset method of modules.
	 */
	VisualNovel.prototype.resetModules = function resetModules() {

		var modules = this.modules;

		for ( var i = modules.length; i--; ) {

			modules[ i ].reset.call( this );

		}

	};

	/**
	 * Function: initNovelContainer
	 *
	 * Initialize novel container template, size, and references.
	 *
	 * @param novelId = id of visual novel div, and instance reference
	 *
	 */
	VisualNovel.prototype.initNovelContainer = function initNovelContainer( novelId ) {

		this.novelContainerId = document.getElementById( novelId );

		var content = this.buildNovelContainerContent( novelId );

		this.setNovelContainerContent( content );

	};

	/**
	 * Function: attachToNovelContainer
	 *
	 * Attach template of new element to the novel container
	 *
	 * @param template = template of element to be attached
	 */
	VisualNovel.prototype.attachToNovelContainer = function attachToNovelContainer( template ) {

		var div = document.createElement( "div" );

		div.innerHTML = template;

		var attachedElement = div.firstChild;

		this.novelContainerId.firstChild.appendChild( attachedElement );

		return attachedElement;

	};

	/**
	 * Variable: novelContainerTemplate
	 *
	 * Template for novel container.
	 */
	VisualNovel.prototype.novelContainerTemplate = "

";

	/**
	 * Function: buildNovelContainer
	 *
	 * Build the html content for the novel container.
	 *
	 * @param novelId = id of visual novel div, and instance reference
	 */
	VisualNovel.prototype.buildNovelContainerContent = function buildNovelContainerContent( novelId ) {

		var content = "";

		if ( this.parser ) {

			content = this.parser.parseTemplate(
					this.novelContainerTemplate,
					{
						"novelId": novelId,
						"width": this.screenWidth,
						"height": this.screenHeight
					}
				);

		} else {

			this.log( "[core]VisualNovelJS Error: parser module not found" );

		}

		return content;

	};

	/**
	 * Function: setNovelContainerContent
	 *
	 * Set the html content of the novel container.
	 *
	 * @param content = content of the novel container
	 */
	VisualNovel.prototype.setNovelContainerContent = function setNovelContainerContent( content ) {

		this.novelContainerId.innerHTML = content;

	};

	/**
	 * Function: log
	 *
	 * Log error message.
	 *
	 * @param msg = error message
	 */
	VisualNovel.prototype.log = function log( msg ) {

		if ( console && console.log ) {

			console.log( msg );

		}

	};

	// Add as global namespace
	w.VisualNovel = VisualNovel;

} )( window );<|endoftext|>


<commit_before>( function( global, factory ) {

    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = factory( global );
    } else {
        factory( global );
    }

}( this, function( global ) {

    function Format() {}

    Format.prototype.format = undefined;

    Format.prototype.parse = undefined;

    global.Format = Format;

    return Format;

} ) );<commit_msg>Define the `Format.Field` object<commit_after>( function( global, factory ) {

    if ( typeof module === "object" && typeof module.exports === "object" ) {
        module.exports = factory( global );
    } else {
        factory( global );
    }

}( this, function( global ) {

    function Format() {}

    Format.prototype.format = function( number ) {
        return undefined;
    };

    Format.prototype.parse = function( source ) {
        return undefined;
    };

    Format.Field = function Field( name ) {
        this.name = name;
    };

    Format.Field.prototype.toString = function() {
        return this.constructor.name + "(" + this.name + ")";
    };

    global.Format = Format;

    return Format;

} ) );<|endoftext|>


<commit_before>const path = require("path");
const builder = require("electron-builder");
const fs = require("fs-extra");
const archiver = require("archiver");
const glob = require("glob");

const projectRoot = path.resolve(__dirname + "/");
const ngDistDir = projectRoot + "/ng-dist";
const electronDir = projectRoot + "/electron";
const appDistDir = projectRoot + "/dist";

// Copy ng-dist to dist
console.log("Copying compiled frontend code...");
fs.copySync(ngDistDir, appDistDir);

// Generate package.json for the distribution build
console.log("Generating production package.json file...");
const electronPackage = JSON.parse(fs.readFileSync(electronDir + "/package.json", "utf8"));
const mainPackage = JSON.parse(fs.readFileSync(projectRoot + "/package.json", "utf8"));
const merged = Object.assign(mainPackage, {
    dependencies: electronPackage.dependencies,
    devDependencies: electronPackage.devDependencies,
    main: "main.js"
});
fs.writeFileSync(appDistDir + "/package.json", JSON.stringify(merged, null, 4));

// Copy Electron main.js file to dist
console.log("Copying main.js...");
fs.copySync(electronDir + "/dist/main.prod.js", appDistDir + "/main.js", {
    overwrite: true
});

// Copy compiled electron code to dist
console.log("Copying electron code...");
fs.copySync(electronDir + "/dist/src", appDistDir + "/src", {
    overwrite: true,
    dereference: true
});
fs.copySync(electronDir + "/executor", appDistDir + "/executor", {
    overwrite: true,
    dereference: true
});
fs.copySync(electronDir + "/src/splash", appDistDir + "/src/splash");

// Copy electron node modules
console.log("Copying electron node_modules...");
fs.copySync(electronDir + "/node_modules", appDistDir + "/node_modules", {
    overwrite: true,
    dereference: true
});

console.log("Starting build process...");

builder.build({
    config: {
        appId: "io.rabix.composer",
        productName: "Rabix Composer",
        asar: true,
        asarUnpack: ["executor/**"],
        directories: {
            output: "build",
            app: "dist",
            buildResources: "build-resources"
        },
        protocols: [{
            "name": "rabix-composer",
            "role": "Editor",
            "schemes": ["rabix-composer"]
        }],
        mac: {
            target: ["dmg"],
        },
        win: {
            target: ["nsis"],
        },
        linux: {
            target: ["AppImage"],
        },
        nsis: {
            oneClick: false,
            perMachine: true,
            allowElevation: true,
        },
        fileAssociations: [{
            ext: "cwl",
            name: "CWL"
        }]

    }
}).then((data) => {

    console.log("Build completed", data);
    /**
     * For Windows, we can't take a single-file artifact because of missing libraries.
     * We need to pack the installer with other stuff that come with the build (.dll libs)
     */
    console.log("Platform: " + process.platform);

    if (process.platform !== "win32") {
        return data;
    }

    console.log("Archiving Windows installer...");
    const [installerFilepath] = glob.sync("*.exe", {cwd: "build"});

    if (!installerFilepath) {
        throw new Error("Cannot find installer binary.");
    }

    const zipPath = "build/" + installerFilepath.slice(0, -3) + "zip";
    const buildInfoPath = "latest.yml";
    const unpackedDir = "win-unpacked";

    const output = fs.createWriteStream(zipPath);
    const archive = archiver("zip");

    output.on("close", () => {
        console.log("Archived " + archive.pointer() + " bytes");
    });

    archive.on("error", err => {
        throw err;
    });

    archive.pipe(output);

    archive.file(`build/${installerFilepath}`, {name: installerFilepath});
    archive.file(`build/${buildInfoPath}`, {name: buildInfoPath});
    archive.directory(`build/${unpackedDir}`, {name: unpackedDir});
    return archive.finalize();

});

console.log("Started building.");

<commit_msg>Revert "turn off directory changing"<commit_after>const path = require("path");
const builder = require("electron-builder");
const fs = require("fs-extra");
const archiver = require("archiver");
const glob = require("glob");

const projectRoot = path.resolve(__dirname + "/");
const ngDistDir = projectRoot + "/ng-dist";
const electronDir = projectRoot + "/electron";
const appDistDir = projectRoot + "/dist";

// Copy ng-dist to dist
console.log("Copying compiled frontend code...");
fs.copySync(ngDistDir, appDistDir);

// Generate package.json for the distribution build
console.log("Generating production package.json file...");
const electronPackage = JSON.parse(fs.readFileSync(electronDir + "/package.json", "utf8"));
const mainPackage = JSON.parse(fs.readFileSync(projectRoot + "/package.json", "utf8"));
const merged = Object.assign(mainPackage, {
    dependencies: electronPackage.dependencies,
    devDependencies: electronPackage.devDependencies,
    main: "main.js"
});
fs.writeFileSync(appDistDir + "/package.json", JSON.stringify(merged, null, 4));

// Copy Electron main.js file to dist
console.log("Copying main.js...");
fs.copySync(electronDir + "/dist/main.prod.js", appDistDir + "/main.js", {
    overwrite: true
});

// Copy compiled electron code to dist
console.log("Copying electron code...");
fs.copySync(electronDir + "/dist/src", appDistDir + "/src", {
    overwrite: true,
    dereference: true
});
fs.copySync(electronDir + "/executor", appDistDir + "/executor", {
    overwrite: true,
    dereference: true
});
fs.copySync(electronDir + "/src/splash", appDistDir + "/src/splash");

// Copy electron node modules
console.log("Copying electron node_modules...");
fs.copySync(electronDir + "/node_modules", appDistDir + "/node_modules", {
    overwrite: true,
    dereference: true
});

console.log("Starting build process...");

builder.build({
    config: {
        appId: "io.rabix.composer",
        productName: "Rabix Composer",
        asar: true,
        asarUnpack: ["executor/**"],
        directories: {
            output: "build",
            app: "dist",
            buildResources: "build-resources"
        },
        protocols: [{
            "name": "rabix-composer",
            "role": "Editor",
            "schemes": ["rabix-composer"]
        }],
        mac: {
            target: ["dmg"],
        },
        win: {
            target: ["nsis"],
        },
        linux: {
            target: ["AppImage"],
        },
        nsis: {
            oneClick: false,
            perMachine: true,
            allowElevation: true,
            allowToChangeInstallationDirectory: true
        },
        fileAssociations: [{
            ext: "cwl",
            name: "CWL"
        }]

    }
}).then((data) => {

    console.log("Build completed", data);
    /**
     * For Windows, we can't take a single-file artifact because of missing libraries.
     * We need to pack the installer with other stuff that come with the build (.dll libs)
     */
    console.log("Platform: " + process.platform);

    if (process.platform !== "win32") {
        return data;
    }

    console.log("Archiving Windows installer...");
    const [installerFilepath] = glob.sync("*.exe", {cwd: "build"});

    if (!installerFilepath) {
        throw new Error("Cannot find installer binary.");
    }

    const zipPath = "build/" + installerFilepath.slice(0, -3) + "zip";
    const buildInfoPath = "latest.yml";
    const unpackedDir = "win-unpacked";

    const output = fs.createWriteStream(zipPath);
    const archive = archiver("zip");

    output.on("close", () => {
        console.log("Archived " + archive.pointer() + " bytes");
    });

    archive.on("error", err => {
        throw err;
    });

    archive.pipe(output);

    archive.file(`build/${installerFilepath}`, {name: installerFilepath});
    archive.file(`build/${buildInfoPath}`, {name: buildInfoPath});
    archive.directory(`build/${unpackedDir}`, {name: unpackedDir});
    return archive.finalize();

});

console.log("Started building.");

<|endoftext|>


<commit_before>/**
 * Imports
 */

import GridSizeSelector from 'components/GridSizeSelector'
import initialGameState from 'utils/initialGameState'
import GameInput from 'components/GameInput'
import {component, element} from 'vdux'
import mapValues from '@f/map-values'
import deepEqual from '@f/deep-equal'
import animalApis from 'animalApis'
import Grid from 'components/Grid'
import setProp from '@f/set-prop'
import unique from '@f/unique'
import {Block} from 'vdux-ui'
import range from '@f/range'
import sleep from '@f/sleep'
import omit from '@f/omit'
import map from '@f/map'

/**
 * <Start Code/>
 */

export default component({
	initialState ({props}) {
		return {...initialGameState, ...props}
	},

	* onCreate ({state, actions}) {
		yield actions.gameDidInitialize(state)
	},

  render ({props, state, actions}) {
  	const {initialData, animals, painted, levelSize, active} = state
  	const btnProps = {
  		bgColor: '#FAFAFA',
  		border: '1px solid #CACACA',
  		sq: 40,
  		color: 'black'
  	}

    return (
    	<Block hide={props.hide} maxWidth={1200} m='0 auto'>
	      <Block pt textAlign='center'>
	        <Block fs='m'color='blue'>Add Your Start Code</Block>
	        <Block fs='xs' my>
	          Students will use the start code to draw the resulting picture on the grid.
	        </Block>
	      </Block>
				<Block flex wide h='600px'>
		    	<Block tall align='start' wide>
		    		{
		    			!props.hideGrid && <Block mr>
				    		<Block bgColor='white' p='1em' border='1px solid divider' borderBottomWidth={0}>
				    			<GridSizeSelector size={levelSize[0]} setSize={actions.setSize} {...btnProps} />
				    		</Block>
				    		<Block maxWidth='900px'>
					    		<Grid
						      	mode='edit'
						        paintMode
						        animals={animals}
						        painted={painted}
						        levelSize='400px'
						        numRows={levelSize[0]}
						        numColumns={levelSize[1]}
						        />
					        </Block>
			        </Block>
		      	}
						<GameInput {...state} gameActions={actions} initialData={initialData} canCode clipboard={state.clipboard} />
					</Block>
				</Block>
			</Block>
    )
  },

  * onUpdate (prev, {props, state, actions}) {
  	if (prev.props.levelSize[0] !== props.levelSize[0])
  	yield actions.gameDidInitialize({
  		...state,
  		animals: props.animals.map((a, i) => ({...a, sequence: state.animals[i].sequence})),
  		levelSize: props.levelSize})
  },

  * onRemove ({actions, props, context, state}) {
  	yield props.save({animals: state.animals.map(a => ({...a, sequence: a.sequence || null}))})
  },

  controller: {
		* setArgument ({state, actions}, block, pos, arg) {
			const {payload = []} = block
			yield actions.setBlockPayload(state.active, block, {...payload, [pos]: arg})
		},
		* setSize ({context, props}, size) {
			yield context.firebaseUpdate(props.ref, {
				levelSize: [size, size],
				animals: props.animals.map(a => ({
					...setProp('current.location', a, [size - 1, 0]),
					sequence: a.sequence || null
				}))
			})
		}
	},

  reducer: {
		reset: (state) => ({
			activeLine: -1,
			painted: state.initialPainted,
			steps: 0,
			selected: [],
			clipboard: [],
			readOnly: false,
			hasRun: false,
			frames: [],
			runners: {},
			running: false,
			pauseState: undefined,
			completed: false,
			animals: state.animals.map(animal => ({...animal, current: animal.initial}))
		}),
  	gameDidInitialize: (state, game) => ({initialData: game, ...game}),
		setCode: (state, id, code) => updateAnimal(state, 'sequence', id, code),
		addBlock: (state, id, block) => addBlock(state, id, {...block}),
		invertSelection: state => ({
			selected: state.selected.length
				? []
				: (state.animals[state.active].sequence || []).filter(b => b.type !== 'repeat_end')
		}),
		removeSelected: state => removeSelected(state),
		cutSelection: state => ({
			...state.selected.reduce((state, block) => removeBlock(state, state.active, block), state),
			selected: [],
			clipboard: expandSelectedLoops(state.animals[state.active].sequence, state.selected),
			editorState: getEditorState(state)
		}),
		copySelection: state => ({
			clipboard: expandSelectedLoops(state.animals[state.active].sequence, state.selected)
		}),
		paste: state => paste(state),
		setBlockPayload: (state, id, block, payload) => ({
			editorState: getEditorState(state),
			...updateAnimal(state, 'sequence', id, state.animals[id].sequence.map(b => b === block ? {...b, payload} : b))
		}),
		setActive: (state, {id}) => ({active: id}),
		setSaved: (state, saved) => ({saved}),
		setActiveLine: (state, activeLine) => ({activeLine}),
		setCursor: (state, cursor) => ({cursor}),
		selectBlock: (state, block) => selectBlock(state, block),
		incrementalPaint: (state, {grid, coord, paintColor}) => setProp(
			`${grid}.${coord}`,
			state,
			state[grid][coord] === paintColor ? 'white' : paintColor
		),
		undo: undo
  }
})

function paste (state) {
	const idxs = state.selected
		.map(b => state.animals[state.active].sequence.indexOf(b))
		.sort(cmp)
	const idx = state.selected.length ? idxs[0] : state.cursor
	const blocks = state.clipboard.map(b => ({...b}))

	if (state.selected.length) {
		state = removeSelected(state)
	}

	return {
		selected: [],
		editorState: getEditorState(state),
		...insertBlocks(state, blocks, idx)
	}
}

function insertBlocks (state, blocks, idx) {
	blocks = [].concat(blocks).filter(Boolean)

	return updateAnimal(
		state,
		'sequence',
		state.active,
		insertAt(state.animals[state.active].sequence || [], idx, blocks.map(b => ({...b})))
	)
}

function removeSelected (state) {
	return {
		...state.selected.reduce((state, block) => removeBlock(state, state.active, block), state),
		editorState: getEditorState(state),
		selected: []
	}
}

function selectBlock (state, block) {
	if (state.selected.length === 0) {
		return {selected: [block]}
	} else if (state.selected.indexOf(block) !== -1) {
		return {selected: []}
	}

	const seq = state.animals[state.active].sequence
	const seqIdx = seq.indexOf(block)
	const selIndices = state
		.selected
		.map(b => seq.indexOf(b))
		.sort(cmp)

	const start = Math.min(selIndices[0], seqIdx)
	const end = Math.max(selIndices[selIndices.length - 1], seqIdx)

	return {
		selected: range(start, end + 1).map(i => seq[i])
	}
}

function undo (state) {
	const {cursor, sequences} = state.editorState[state.editorState.length - 1]
	return {
		animals: state.animals.map((a, i) => ({...a, sequence: sequences[i]})),
		editorState: state.editorState.slice(0, -1),
		selected: [],
		cursor
	}
}

function getSequences (animals) {
	return animals.map((a) => a.sequence)
}

function isEqualSequence (a, b) {
	return deepEqual(getSequences(a), getSequences(b))
}

function getEditorState (state) {
	return state.editorState.concat({sequences: getSequences(state.animals), cursor: state.cursor})
}

function addBlock (state, id, block) {
	const {cursor, selected, animals} = state

	if (block.type === 'repeat') {
		if (selected.length) {
			return surroundBlocks(state, id, selected, block)
		}

		return {
			...insertBlocks(state, [block, {type: 'repeat_end'}], cursor),
			cursor: cursor + 1,
			editorState: getEditorState(state)
		}
	} else if (selected.length) {
		const idxs = selected
			.map(b => state.animals[state.active].sequence.indexOf(b))
			.sort(cmp)

		state = removeSelected(state)

		return {
			...state,
			...insertBlocks(state, block, idxs[0]),
			cursor: state.cursor > state.animals[id].sequence.length
				? state.animals[id].sequence.length - 1
				: state.cursor,
			selected: []
		}
	}

	return {
		...insertBlocks(state, block, cursor),
		cursor: cursor + 1,
		editorState: getEditorState(state)
	}
}

function surroundBlocks (state, id, selected, block) {
	selected = expandSelectedLoops(state.animals[id].sequence, selected)

	const idxs = selected
		.map(b => state.animals[id].sequence.indexOf(b))
		.sort(cmp)

	let start = idxs[0]
	let inserted = 0
	let sequence = state.animals[id].sequence.slice()

	for (let i = 1; i < idxs.length; i++) {
		if (idxs[i] > idxs[i-1] + 1) {
			sequence.splice((start + inserted++), 0, {...block})
			sequence.splice(idxs[i-1] + 1 + (inserted++), 0, {type: 'repeat_end'})
			start = idxs[i]
		}
	}

	sequence.splice((start + inserted++), 0, {...block})
	sequence.splice(idxs[idxs.length - 1] + 1 + (inserted++), 0, {type: 'repeat_end'})

	return {
		...updateAnimal(state, 'sequence', id, sequence),
		selected: [],
		cursor: state.cursor + 1,
		editorState: getEditorState(state)
	}
}

function cmp (a, b) {
	return a === b ? 0 : a < b ? -1 : 1
}

/**
 * expandSelectedLoops
 *
 * Expand the selection to include the entirety of any loops
 * that have been selected
 */

function expandSelectedLoops (sequence, selected) {
	return unique(selected.reduce((acc, block) => {
		if (block.type === 'repeat') {
			const idx = sequence.indexOf(block)
			const loop = [block]
			let level = 0

			for (let i = idx + 1; i < sequence.length; i++) {
				const item = sequence[i]
				loop.push(item)

				if (item.type === 'repeat') {
					level++
				} else if (item.type === 'repeat_end') {
					if (level === 0) {
						break
					} else {
						level--
					}
				}
			}

			return acc.concat(unique(loop))
		}

		return acc.concat(block)
	}, []))
}

function updateAnimal (state, path, id, val) {
	return {
		animals: state.animals.map((animal, i) => {
			return i === id
				? setProp(path, animal, val)
				: animal
		}),
		cursor: Math.min((state.animals[state.active].sequence || []).length, state.cursor)
	}
}

function insertAt (list, idx, val) {
	list = list.slice()
	Array.isArray(val)
		? list.splice(idx, 0, ...val)
		: list.splice(idx, 0, val)
	return list
}

function wrapEffect (fn) {
  return (model, ...args) => fn(...args)
}

function removeBlock (state, id, block) {
	if (block.type === 'repeat') {
		const idx = state.animals[id].sequence.indexOf(block)
		let endBlock
		let level = 0

		for (let i = idx + 1; i < state.animals[id].sequence.length; i++) {
			const item = state.animals[id].sequence[i]
			if (item.type === 'repeat') level++
			if (item.type === 'repeat_end') {
				if (level === 0) {
					endBlock = item
					break
				}

				level--
			}
		}

		if (endBlock) {
			state = removeBlock(state, id, endBlock)
		}
	}

	const nextState = updateAnimal(state, 'sequence', id, state.animals[id].sequence.filter(b => b !== block))

	return {
		...state,
		...nextState,
		activeLine: Math.min(state.activeLine, nextState.animals[id].sequence.length),
		cursor: Math.min(state.cursor, nextState.animals[id].sequence.length)
	}
}
<commit_msg>save start location and rotation<commit_after>/**
 * Imports
 */

import GridSizeSelector from 'components/GridSizeSelector'
import initialGameState from 'utils/initialGameState'
import GameInput from 'components/GameInput'
import {component, element} from 'vdux'
import mapValues from '@f/map-values'
import deepEqual from '@f/deep-equal'
import animalApis from 'animalApis'
import Grid from 'components/Grid'
import setProp from '@f/set-prop'
import unique from '@f/unique'
import {Block} from 'vdux-ui'
import range from '@f/range'
import sleep from '@f/sleep'
import omit from '@f/omit'
import map from '@f/map'

/**
 * <Start Code/>
 */

export default component({
	initialState ({props}) {
		return {...initialGameState, ...props}
	},

	* onCreate ({state, actions}) {
		yield actions.gameDidInitialize(state)
	},

  render ({props, state, actions}) {
  	const {initialData, animals, painted, levelSize, active} = state
  	const btnProps = {
  		bgColor: '#FAFAFA',
  		border: '1px solid #CACACA',
  		sq: 40,
  		color: 'black'
  	}

    return (
    	<Block hide={props.hide} maxWidth={1200} m='0 auto'>
	      <Block pt textAlign='center'>
	        <Block fs='m'color='blue'>Add Your Start Code</Block>
	        <Block fs='xs' my>
	          Students will use the start code to draw the resulting picture on the grid.
	        </Block>
	      </Block>
				<Block flex wide h='600px'>
		    	<Block tall align='start' wide>
		    		{
		    			!props.hideGrid && <Block mr>
				    		<Block bgColor='white' p='1em' border='1px solid divider' borderBottomWidth={0}>
				    			<GridSizeSelector size={levelSize[0]} setSize={actions.setSize} {...btnProps} />
				    		</Block>
				    		<Block maxWidth='900px'>
					    		<Grid
						      	mode='edit'
						        paintMode
						        animals={animals}
						        painted={painted}
						        levelSize='400px'
						        numRows={levelSize[0]}
						        numColumns={levelSize[1]}
						        />
					        </Block>
			        </Block>
		      	}
						<GameInput {...state} gameActions={actions} initialData={initialData} canCode clipboard={state.clipboard} />
					</Block>
				</Block>
			</Block>
    )
  },

  * onUpdate (prev, {props, state, actions}) {
  	if (prev.props.levelSize[0] !== props.levelSize[0])
  	yield actions.gameDidInitialize({
  		...state,
  		animals: props.animals.map((a, i) => ({...a, sequence: state.animals[i].sequence})),
  		levelSize: props.levelSize})
  },

  * onRemove ({actions, props, context, state}) {
  	yield props.save({animals: state.animals.map((a, i) => ({...props.animals[i], sequence: a.sequence || null}))})
  },

  controller: {
		* setArgument ({state, actions}, block, pos, arg) {
			const {payload = []} = block
			yield actions.setBlockPayload(state.active, block, {...payload, [pos]: arg})
		},
		* setSize ({context, props}, size) {
			yield context.firebaseUpdate(props.ref, {
				levelSize: [size, size],
				animals: props.animals.map(a => ({
					...setProp('current.location', a, [size - 1, 0]),
					sequence: a.sequence || null
				}))
			})
		}
	},

  reducer: {
		reset: (state) => ({
			activeLine: -1,
			painted: state.initialPainted,
			steps: 0,
			selected: [],
			clipboard: [],
			readOnly: false,
			hasRun: false,
			frames: [],
			runners: {},
			running: false,
			pauseState: undefined,
			completed: false,
			animals: state.animals.map(animal => ({...animal, current: animal.initial}))
		}),
  	gameDidInitialize: (state, game) => ({initialData: game, ...game}),
		setCode: (state, id, code) => updateAnimal(state, 'sequence', id, code),
		addBlock: (state, id, block) => addBlock(state, id, {...block}),
		invertSelection: state => ({
			selected: state.selected.length
				? []
				: (state.animals[state.active].sequence || []).filter(b => b.type !== 'repeat_end')
		}),
		removeSelected: state => removeSelected(state),
		cutSelection: state => ({
			...state.selected.reduce((state, block) => removeBlock(state, state.active, block), state),
			selected: [],
			clipboard: expandSelectedLoops(state.animals[state.active].sequence, state.selected),
			editorState: getEditorState(state)
		}),
		copySelection: state => ({
			clipboard: expandSelectedLoops(state.animals[state.active].sequence, state.selected)
		}),
		paste: state => paste(state),
		setBlockPayload: (state, id, block, payload) => ({
			editorState: getEditorState(state),
			...updateAnimal(state, 'sequence', id, state.animals[id].sequence.map(b => b === block ? {...b, payload} : b))
		}),
		setActive: (state, {id}) => ({active: id}),
		setSaved: (state, saved) => ({saved}),
		setActiveLine: (state, activeLine) => ({activeLine}),
		setCursor: (state, cursor) => ({cursor}),
		selectBlock: (state, block) => selectBlock(state, block),
		incrementalPaint: (state, {grid, coord, paintColor}) => setProp(
			`${grid}.${coord}`,
			state,
			state[grid][coord] === paintColor ? 'white' : paintColor
		),
		undo: undo
  }
})

function paste (state) {
	const idxs = state.selected
		.map(b => state.animals[state.active].sequence.indexOf(b))
		.sort(cmp)
	const idx = state.selected.length ? idxs[0] : state.cursor
	const blocks = state.clipboard.map(b => ({...b}))

	if (state.selected.length) {
		state = removeSelected(state)
	}

	return {
		selected: [],
		editorState: getEditorState(state),
		...insertBlocks(state, blocks, idx)
	}
}

function insertBlocks (state, blocks, idx) {
	blocks = [].concat(blocks).filter(Boolean)

	return updateAnimal(
		state,
		'sequence',
		state.active,
		insertAt(state.animals[state.active].sequence || [], idx, blocks.map(b => ({...b})))
	)
}

function removeSelected (state) {
	return {
		...state.selected.reduce((state, block) => removeBlock(state, state.active, block), state),
		editorState: getEditorState(state),
		selected: []
	}
}

function selectBlock (state, block) {
	if (state.selected.length === 0) {
		return {selected: [block]}
	} else if (state.selected.indexOf(block) !== -1) {
		return {selected: []}
	}

	const seq = state.animals[state.active].sequence
	const seqIdx = seq.indexOf(block)
	const selIndices = state
		.selected
		.map(b => seq.indexOf(b))
		.sort(cmp)

	const start = Math.min(selIndices[0], seqIdx)
	const end = Math.max(selIndices[selIndices.length - 1], seqIdx)

	return {
		selected: range(start, end + 1).map(i => seq[i])
	}
}

function undo (state) {
	const {cursor, sequences} = state.editorState[state.editorState.length - 1]
	return {
		animals: state.animals.map((a, i) => ({...a, sequence: sequences[i]})),
		editorState: state.editorState.slice(0, -1),
		selected: [],
		cursor
	}
}

function getSequences (animals) {
	return animals.map((a) => a.sequence)
}

function isEqualSequence (a, b) {
	return deepEqual(getSequences(a), getSequences(b))
}

function getEditorState (state) {
	return state.editorState.concat({sequences: getSequences(state.animals), cursor: state.cursor})
}

function addBlock (state, id, block) {
	const {cursor, selected, animals} = state

	if (block.type === 'repeat') {
		if (selected.length) {
			return surroundBlocks(state, id, selected, block)
		}

		return {
			...insertBlocks(state, [block, {type: 'repeat_end'}], cursor),
			cursor: cursor + 1,
			editorState: getEditorState(state)
		}
	} else if (selected.length) {
		const idxs = selected
			.map(b => state.animals[state.active].sequence.indexOf(b))
			.sort(cmp)

		state = removeSelected(state)

		return {
			...state,
			...insertBlocks(state, block, idxs[0]),
			cursor: state.cursor > state.animals[id].sequence.length
				? state.animals[id].sequence.length - 1
				: state.cursor,
			selected: []
		}
	}

	return {
		...insertBlocks(state, block, cursor),
		cursor: cursor + 1,
		editorState: getEditorState(state)
	}
}

function surroundBlocks (state, id, selected, block) {
	selected = expandSelectedLoops(state.animals[id].sequence, selected)

	const idxs = selected
		.map(b => state.animals[id].sequence.indexOf(b))
		.sort(cmp)

	let start = idxs[0]
	let inserted = 0
	let sequence = state.animals[id].sequence.slice()

	for (let i = 1; i < idxs.length; i++) {
		if (idxs[i] > idxs[i-1] + 1) {
			sequence.splice((start + inserted++), 0, {...block})
			sequence.splice(idxs[i-1] + 1 + (inserted++), 0, {type: 'repeat_end'})
			start = idxs[i]
		}
	}

	sequence.splice((start + inserted++), 0, {...block})
	sequence.splice(idxs[idxs.length - 1] + 1 + (inserted++), 0, {type: 'repeat_end'})

	return {
		...updateAnimal(state, 'sequence', id, sequence),
		selected: [],
		cursor: state.cursor + 1,
		editorState: getEditorState(state)
	}
}

function cmp (a, b) {
	return a === b ? 0 : a < b ? -1 : 1
}

/**
 * expandSelectedLoops
 *
 * Expand the selection to include the entirety of any loops
 * that have been selected
 */

function expandSelectedLoops (sequence, selected) {
	return unique(selected.reduce((acc, block) => {
		if (block.type === 'repeat') {
			const idx = sequence.indexOf(block)
			const loop = [block]
			let level = 0

			for (let i = idx + 1; i < sequence.length; i++) {
				const item = sequence[i]
				loop.push(item)

				if (item.type === 'repeat') {
					level++
				} else if (item.type === 'repeat_end') {
					if (level === 0) {
						break
					} else {
						level--
					}
				}
			}

			return acc.concat(unique(loop))
		}

		return acc.concat(block)
	}, []))
}

function updateAnimal (state, path, id, val) {
	return {
		animals: state.animals.map((animal, i) => {
			return i === id
				? setProp(path, animal, val)
				: animal
		}),
		cursor: Math.min((state.animals[state.active].sequence || []).length, state.cursor)
	}
}

function insertAt (list, idx, val) {
	list = list.slice()
	Array.isArray(val)
		? list.splice(idx, 0, ...val)
		: list.splice(idx, 0, val)
	return list
}

function wrapEffect (fn) {
  return (model, ...args) => fn(...args)
}

function removeBlock (state, id, block) {
	if (block.type === 'repeat') {
		const idx = state.animals[id].sequence.indexOf(block)
		let endBlock
		let level = 0

		for (let i = idx + 1; i < state.animals[id].sequence.length; i++) {
			const item = state.animals[id].sequence[i]
			if (item.type === 'repeat') level++
			if (item.type === 'repeat_end') {
				if (level === 0) {
					endBlock = item
					break
				}

				level--
			}
		}

		if (endBlock) {
			state = removeBlock(state, id, endBlock)
		}
	}

	const nextState = updateAnimal(state, 'sequence', id, state.animals[id].sequence.filter(b => b !== block))

	return {
		...state,
		...nextState,
		activeLine: Math.min(state.activeLine, nextState.animals[id].sequence.length),
		cursor: Math.min(state.cursor, nextState.animals[id].sequence.length)
	}
}
<|endoftext|>


<commit_before>import app from "../../src/reducers/App.js";
import game from "../../src/reducers/Game.js";
import screen from "../../src/reducers/Screen.js";
import {setCurrentScreenAction, MENU_SCREEN, GAME_SCREEN} from "../../src/ScreenActions.js";
import gridChangedAction from "../../src/actions/GameActions.js";

describe("app reducer", () => {

  it("should exist", () => {
    app.should.exist;
  });

  it("should be a function", () => {
    app.should.be.function;
  });

  describe("should work as a combination of two reducers", () => {

    const initialState = {
      screen: "MENU_SCREEN",
      game: {
        gameGrid: [["","",""],
                   ["","",""],
                   ["","",""]],
        currentTurn: "x",
        victoryStatistics: {
          x: 0,
          o: 0
        },
        gameOver: false
      }
    };

    var state,
        previousState,
        setMenuScreenAction = setCurrentScreenAction(MENU_SCREEN),
        setGameScreenAction = setCurrentScreenAction(GAME_SCREEN),
        girdChangedAt00 = gridChangedAction(0,0),
        girdChangedAt11 = gridChangedAction(0,0);

    it("should return initial state if state is empty", () => {
      state = app();
      state.should.be.equal(initialState);
    });

    it("should return the same state if no action", () => {
      previousState = state;
      state = app(state, {type: "bla-bla-bla"});
      previousState.should.be.eql(state);
    });

    it("should handle screen actions with screen reducer", () => {
      previousState = state;
      state = app(state, setGameScreenAction);
      state.should.be.not.eql(previousState);
      state.screen.should.be.equal(screen(previousState, setGameScreenAction));

      previousState = state;
      state = app(state, setMenuScreenAction);
      state.should.be.not.eql(previousState);
      state.screen.should.be.equal(screen(previousState, setMenuScreenAction));
    });

    it("should handle screen actions with screen reducer", () => {
      previousState = state;
      state = app(state, setGameScreenAction);
      state.should.be.not.eql(previousState);
      state.screen.should.be.equal(screen(previousState, setGameScreenAction));

      previousState = state;
      state = app(state, setMenuScreenAction);
      state.should.be.not.eql(previousState);
      state.screen.should.be.equal(screen(previousState, setMenuScreenAction));
    });

    it("should handle game actions with game reducer", () => {
      previousState = state;
      state = app(state, girdChangedAt00);
      state.should.be.not.eql(previousState);
      state.game.should.be.equal(game(previousState, girdChangedAt00));

      previousState = state;
      state = app(state, girdChangedAt00);
      state.should.be.eql(previousState);
      state.game.should.be.equal(game(previousState, girdChangedAt00));

      previousState = state;
      state = app(state, girdChangedAt11);
      state.should.be.not.eql(previousState);
      state.game.should.be.equal(game(previousState, girdChangedAt11));
    });

  });

});
<commit_msg>Fix import issues, new action creator names, and some wrong tests<commit_after>import app from "../../src/reducers/App.js";
import game from "../../src/reducers/Game.js";
import screen from "../../src/reducers/Screen.js";
import setCurrentScreenAction from "../../src/actions/ScreenActions.js";
import { MENU_SCREEN, GAME_SCREEN } from "../../src/actions/ScreenActions.js";
import changeGridAction from "../../src/actions/GameActions.js";
import { initialGameState } from "../../src/reducers/Game.js";

describe("app reducer", () => {

  it("should exist", () => {
    app.should.exist;
  });

  it("should be a function", () => {
    app.should.be.function;
  });

  describe("should work as a combination of two reducers", () => {

    var state,
        previousState,
        setMenuScreenAction = setCurrentScreenAction(MENU_SCREEN),
        setGameScreenAction = setCurrentScreenAction(GAME_SCREEN),
        girdChangedAt00 = changeGridAction(0,0),
        girdChangedAt11 = changeGridAction(0,0);

    it("should return initial state if state is empty", () => {
      state = app(undefined, {type: "this is test"});
      state.should.be.eql({
        screen: MENU_SCREEN,
        game: initialGameState()
      });
    });

    it("should return the same state if no action", () => {
      previousState = state;
      state = app(state, {type: "bla-bla-bla"});
      previousState.should.be.equal(state);
    });

    it("should handle screen actions with screen reducer", () => {
      previousState = app(undefined, {type: "this is test"});
      state = app(undefined, {type: "this is test"});
      state = app(state, setGameScreenAction);
      state.should.be.not.equal(previousState);
      state.screen.should.be.equal(screen(previousState, setGameScreenAction));

      previousState = app(undefined, {type: "this is test"});
      state = app(undefined, {type: "this is test"});
      state = app(state, setMenuScreenAction);
      state.should.be.not.equal(previousState);
      state.screen.should.be.equal(screen(previousState, setMenuScreenAction));
    });

    it("should handle screen actions with screen reducer", () => {
      previousState = state;
      state = app(state, setGameScreenAction);
      state.should.be.not.eql(previousState);
      state.screen.should.be.equal(screen(previousState.screen,
        setGameScreenAction));

      previousState = state;
      state = app(state, setMenuScreenAction);
      state.should.be.not.eql(previousState);
      state.screen.should.be.equal(screen(previousState.screen,
        setMenuScreenAction));
    });

    it("should handle game actions with game reducer", () => {
      previousState = state;
      state.game = initialGameState();
      previousState = initialGameState();

      state = app(state, girdChangedAt00);
      previousState.game = game(previousState.game, girdChangedAt00);
      state.game.should.be.eql(previousState.game);

      state = app(state, girdChangedAt00);
      previousState.game = game(previousState.game, girdChangedAt00);
      state.game.should.be.eql(previousState.game);

      state = app(state, girdChangedAt11);
      previousState.game = game(previousState.game, girdChangedAt00);
      state.game.should.be.eql(previousState.game);
    });

  });

});
<|endoftext|>