"use strict";
/**
 * Mongo DB DAO for program server
 *
 *  Copyright (C) 2010-2019 R-T Specialty, LLC.
 *
 *  This file is part of the Liza Data Collection Framework.
 *
 *  liza is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MongoServerDao = exports.asyncServerDao = exports.ServerDaoSaveError = void 0;
var NoPendingError_1 = require("../../error/NoPendingError");
var events_1 = require("events");
var ServerDaoSaveError = /** @class */ (function (_super) {
    __extends(ServerDaoSaveError, _super);
    /**
     * @param message - error message
     * @param id      - data model's ID
     */
    function ServerDaoSaveError(message, id) {
        var _newTarget = this.constructor;
        var _this = _super.call(this, message) || this;
        _this.id = id;
        Object.setPrototypeOf(_this, _newTarget.prototype);
        _this.name = ServerDaoSaveError.name;
        return _this;
    }
    return ServerDaoSaveError;
}(Error));
exports.ServerDaoSaveError = ServerDaoSaveError;
/**
 * Convert a callback-based ServerDao to be a promise-based
 *
 * @param dao - relevant DAO
 *
 * @return promise-based API
 */
exports.asyncServerDao = function (dao) {
    return {
        /**
         * Save the quote
         *
         * @param quote - quote
         * @param data  - saves this data instead of the bucket
         * @param push_data - quote data to push
         *
         * @return promise with quote
         */
        saveQuote: function (quote, data, push_data) {
            return __awaiter(this, void 0, void 0, function () {
                var on_failure;
                return __generator(this, function (_a) {
                    on_failure = function () {
                        throw new ServerDaoSaveError('Failed to save quote.', quote.getId());
                    };
                    return [2 /*return*/, new Promise(function (resolve) {
                            dao.saveQuote(quote, resolve, on_failure, data, push_data);
                        })];
                });
            });
        },
        /**
         * Save the quote metabucket directly
         *
         * @param quote    - quote
         * @param new_meta - saves this data instead of the metabucket
         *
         * @return promise with quote
         */
        saveQuoteMetaDirect: function (quote, new_meta) {
            return __awaiter(this, void 0, void 0, function () {
                var on_failure;
                return __generator(this, function (_a) {
                    on_failure = function () {
                        throw new ServerDaoSaveError('Failed to save quote meta.', quote.getId());
                    };
                    return [2 /*return*/, new Promise(function (resolve) {
                            dao.saveQuoteMetaDirect(quote, new_meta, resolve, on_failure);
                        })];
                });
            });
        },
    };
};
/**
 * Uses MongoDB as a data store
 */
var MongoServerDao = /** @class */ (function (_super) {
    __extends(MongoServerDao, _super);
    /**
     * Initializes DAO
     *
     * @param _quote_collection - collection to save data to
     * @param _seq_collection   - collection to read sequences (auto-increments)
     * @param _env              - the name of the current environment
     * @param _ts_ctor          - a timestamp constructor
     */
    function MongoServerDao(_quote_collection, _seq_collection, _env, _ts_ctor) {
        var _this = _super.call(this) || this;
        _this._quote_collection = _quote_collection;
        _this._seq_collection = _seq_collection;
        _this._env = _env;
        _this._ts_ctor = _ts_ctor;
        /** Collection used to store quotes */
        _this.COLLECTION = 'quotes';
        /** Sequence (auto-increment) collection */
        _this.COLLECTION_SEQ = 'seq';
        /** Sequence key for quote ids */
        _this.SEQ_QUOTE_ID = 'quoteId';
        /** Sequence quoteId default */
        _this.SEQ_QUOTE_ID_DEFAULT = 200000;
        /** Whether the DAO is initialized and ready to be used */
        _this._ready = true;
        /** The document fields to read */
        _this.PULL_FIELDS = {
            agentEntityId: 1,
            agentId: 1,
            agentName: 1,
            boundInd: 1,
            creditScoreRef: 1,
            currentStepId: 1,
            data: 1,
            error: 1,
            explicitLock: 1,
            explicitLockStepId: 1,
            importDirty: 1,
            importedInd: 1,
            initialRatedDate: 1,
            lastPremDate: 1,
            meta: 1,
            pver: 1,
            ratedata: 1,
            retryAttempts: 1,
            startDate: 1,
            topSavedStepId: 1,
            topVisitedStepId: 1,
            fieldState: 1,
        };
        _this.nonpersistent_data_keys = ['__internal_user'];
        return _this;
    }
    /**
     * Initializes error events and attempts to connect to the database
     *
     * connectError event will be emitted on failure.
     *
     * @param success - function to call when connection is complete
     *                          (will not be called if connection fails)
     */
    MongoServerDao.prototype.init = function (success) {
        var _this = this;
        this._seq_collection
            .find({ _id: this.SEQ_QUOTE_ID })
            .limit(1)
            .next(function (err, data) {
            if (err || !data) {
                _this._initQuoteIdSeq(success);
                _this.emit('ready');
                return;
            }
            if (success instanceof Function) {
                success();
            }
        });
        return this;
    };
    /**
     * Initialize the quote id sequence collection
     *
     * @param success - a function to call once it has been initialized
     */
    MongoServerDao.prototype._initQuoteIdSeq = function (success) {
        var _this = this;
        this._seq_collection.insertOne({
            _id: this.SEQ_QUOTE_ID,
            val: this.SEQ_QUOTE_ID_DEFAULT,
        }, function (err) {
            if (err) {
                _this.emit('seqError', err);
                return;
            }
            _this.emit('seqInit', _this.SEQ_QUOTE_ID);
            success();
        });
    };
    /**
     * Saves a quote to the database
     *
     * A full save will include all metadata.  This should not cause any
     * problems with race conditions for pending Data API calls on meta
     * fields because those results write to individual indexes and do not
     * rely on existing data.
     *
     * @param quote         - the quote to save
     * @param success       - function to call on success
     * @param failure       - function to call if save fails
     * @param save_data     - quote data to save (optional)
     * @param push_data     - quote data to push (optional)
     * @param force_publish - reset published indicator (optional)
     */
    MongoServerDao.prototype.saveQuote = function (quote, success, failure, save_data, push_data, force_publish) {
        var _this = this;
        var _a, _b, _c, _d;
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (force_publish === void 0) { force_publish = true; }
        // if we're not ready, then we can't save the quote!
        if (this._ready === false) {
            this.emit('saveQuoteError', { message: 'Database server not ready' }, Error('Database not ready'), quote);
            failure.call(this, quote);
            return this;
        }
        if (save_data === undefined) {
            save_data = {};
            var quote_data_1 = quote.getBucket().getData();
            Object.keys(quote_data_1).forEach(function (key) { return (save_data['data.' + key] = quote_data_1[key]); });
        }
        var id = quote.getId();
        var save_ts = this._ts_ctor();
        // some data should always be saved because the quote will be created if
        // it does not yet exist
        save_data.id = id;
        save_data.env = this._env;
        save_data.pver = quote.getProgramVersion();
        save_data.importDirty = 1;
        save_data.lastPremDate = quote.getLastPremiumDate();
        save_data.retryAttempts = quote.getRetryAttempts();
        save_data.initialRatedDate = quote.getRatedDate();
        save_data.explicitLock = quote.getExplicitLockReason();
        save_data.explicitLockStepId = quote.getExplicitLockStep();
        save_data.importedInd = +quote.isImported();
        save_data.boundInd = +quote.isBound();
        save_data.lastUpdate = save_ts;
        save_data.quoteSetId = id;
        // Attempt to set these fields if they have not been provided ad hoc
        save_data.startDate = (_a = save_data.startDate) !== null && _a !== void 0 ? _a : quote.getStartDate();
        save_data.agentId = '' + ((_b = save_data.agentId) !== null && _b !== void 0 ? _b : quote.getAgentId());
        save_data.agentName = (_c = save_data.agentName) !== null && _c !== void 0 ? _c : quote.getAgentName();
        save_data.agentEntityId = (_d = save_data.agentEntityId) !== null && _d !== void 0 ? _d : quote.getAgentEntityId();
        if (force_publish) {
            save_data.publishResetTs = save_ts;
            save_data.published = false;
        }
        var exp_date = quote.getExpirationDate();
        save_data.quoteExpDate = exp_date === Infinity ? 0 : exp_date;
        // meta will eventually take over for much of the above data
        save_data['meta.liza_timestamp_initial_rated'] = [quote.getRatedDate()];
        var last_updated_by = quote.getUserName();
        if (last_updated_by !== '') {
            save_data['meta.last_updated_by_username'] = [last_updated_by];
        }
        // save the stack so we can track this call via the oplog
        save_data._stack = new Error().stack;
        // only set created by on initial document insert
        var insert_only = {
            'meta.created_by_username': [quote.getUserName()],
            'meta.retail_agency': [quote.getAgencyNumber()],
        };
        // only set bind username on concluding save
        if (quote.isImported() && last_updated_by !== '') {
            save_data['meta.bound_by_username'] = [quote.getUserName()];
        }
        save_data = this.filterNonpersistent(save_data);
        // do not push empty objects
        var document = !push_data || !Object.keys(push_data).length
            ? { $set: save_data, $setOnInsert: insert_only }
            : { $set: save_data, $setOnInsert: insert_only, $push: push_data };
        // update the quote data if it already exists (same id), otherwise
        // insert it
        this._quote_collection.updateOne({ id: id }, document, 
        // create record if it does not yet exist
        { upsert: true }, 
        // on complete
        function (err) {
            // if an error occurred, then we cannot continue
            if (err) {
                _this.emit('saveQuoteError', err, quote);
                // let the caller handle the error
                if (failure instanceof Function) {
                    failure(quote);
                }
                return;
            }
            // successful
            if (success instanceof Function) {
                success.call(_this, quote);
            }
        });
        return this;
    };
    /**
     * Merges quote data with the existing (rather than overwriting)
     *
     * @param quote   - quote to save
     * @param data    - quote data
     * @param success - successful callback
     * @param failure - failure callback
     * @param options - mongo options to specify
     */
    MongoServerDao.prototype.mergeData = function (quote, data, success, failure, options) {
        var _this = this;
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (options === void 0) { options = {}; }
        if (!data || !Object.keys(data).length) {
            success(quote);
            return this;
        }
        // we do not want to alter the original data; use it as a prototype
        var update = this.filterNonpersistent(data);
        // save the stack so we can track this call via the oplog
        this._quote_collection.updateOne({ id: quote.getId() }, { $set: update }, options, function (err) {
            if (err) {
                _this.emit('saveQuoteError', err, quote);
                if (typeof failure === 'function') {
                    failure(quote);
                }
                return;
            }
            if (typeof success === 'function') {
                success(quote);
            }
        });
        return this;
    };
    /**
     * Filter out nonpersistent data
     *
     * FIXME: This is a temporary solution until nonpersistent fields are defined
     * elsewhere in a more formal way
     *
     * @param data The bucket data to filter
     */
    MongoServerDao.prototype.filterNonpersistent = function (data) {
        for (var _i = 0, _a = this.nonpersistent_data_keys; _i < _a.length; _i++) {
            var key = _a[_i];
            delete data['data.' + key];
        }
        return data;
    };
    /**
     * Merges bucket data with the existing bucket (rather than overwriting the
     * entire bucket)
     *
     * @param quote   - quote to save
     * @param data    - bucket data
     * @param success - successful callback
     * @param failure - failure callback
     */
    MongoServerDao.prototype.mergeBucket = function (quote, data, success, failure) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        var update = {};
        for (var field in data) {
            if (!field) {
                continue;
            }
            update['data.' + field] = data[field];
        }
        return this.mergeData(quote, update, success, failure);
    };
    /**
     * Saves the quote state to the database
     *
     * The quote state includes the current step, the top visited step and the
     * explicit lock message.
     *
     * @param quote   - the quote to save
     * @param success - function to call on success
     * @param failure - function to call if save fails
     */
    MongoServerDao.prototype.saveQuoteState = function (quote, success, failure) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        var update = {
            currentStepId: quote.getCurrentStepId(),
            topVisitedStepId: quote.getTopVisitedStepId(),
            topSavedStepId: quote.getTopSavedStepId(),
        };
        var field_state = quote.getFieldState();
        // Do not wipe out field state if it's not provided (having
        // getFieldState run the classifier automatically would be dangerous
        // since it is so expensive)
        if (field_state && Object.keys(field_state).length > 0) {
            update.fieldState = field_state;
        }
        return this.mergeData(quote, update, success, failure);
    };
    /**
     * Ensure the quote has pending rates
     *
     * @param quote - the quote to validate
     *
     * @returns a promise with the quote
     */
    MongoServerDao.prototype.ensurePendingRate = function (quote) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            _this._quote_collection
                .find({ id: quote.getId() })
                .limit(1)
                .project({ 'ratedata.__rate_pending': 1 })
                .next(function (err, data) {
                // was the quote found?
                if (!data || err) {
                    // Return the quote unchanged
                    reject(new Error('Could not find quote ' + quote.getId()));
                    return;
                }
                var ratedata = data.ratedata;
                if (!ratedata) {
                    reject(new NoPendingError_1.NoPendingError('No prior rate for quote ' + quote.getId()));
                    return;
                }
                if (!ratedata.__rate_pending || ratedata.__rate_pending <= 0) {
                    reject(new NoPendingError_1.NoPendingError('No pending rates for quote ' + quote.getId()));
                    return;
                }
                resolve(quote);
            });
        });
    };
    /**
     * Sets whether the quote is published to the retail agent
     *
     * if published the liza_access_group is set to the retail agency on the quote
     * if unpublished the liza_access_group is cleared
     *
     * @param quote    - destination quote
     * @param publish  - true or false
     */
    MongoServerDao.prototype.publishQuote = function (quote, publish) {
        var update = {};
        var meta = quote.getMetabucket().getData();
        update['meta.liza_timestamp_owner_vis_updated'] = [this._ts_ctor()];
        if (publish === true) {
            update['meta.liza_access_groups'] =
                meta.retail_agency === undefined ? undefined : meta.retail_agency;
        }
        else {
            update['meta.liza_access_groups'] = [];
        }
        return this._quote_collection.updateOne({ id: quote.getId() }, { $set: update });
    };
    /**
     * Save document metadata (meta field on document)
     *
     * Only the provided indexes will be modified (that is---data will be
     * merged with what is already in the database).
     *
     * @param quote    - destination quote
     * @param new_meta - bucket-formatted data to write
     * @param success  - callback on success
     * @param failure  - callback on error
     * @param options  - mongo options to specify
     */
    MongoServerDao.prototype.saveQuoteMeta = function (quote, new_meta, success, failure, options) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (options === void 0) { options = {}; }
        var update = {};
        new_meta = new_meta || quote.getMetabucket().getData();
        for (var key in new_meta) {
            var meta = new_meta[key];
            for (var i in meta) {
                update['meta.' + key + '.' + i] = new_meta[key][i];
            }
        }
        this.mergeData(quote, update, success, failure, options);
    };
    /**
     * Save document metadata directly
     *
     * This saves data directly to the top-level keys of the meta bucket and does
     * not iterate over any values that are iterable. These values will be saved
     * as-is.
     *
     * TODO: Replace saveQuoteMeta with this method when bugs related to not
     * iterating over inner-indexes are alleviated.
     *
     * @param quote    - destination quote
     * @param new_meta - bucket-formatted data to write
     * @param success  - callback on success
     * @param failure  - callback on error
     * @param options  - mongo options to specify
     */
    MongoServerDao.prototype.saveQuoteMetaDirect = function (quote, new_meta, success, failure, options) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        if (options === void 0) { options = {}; }
        var update = {};
        new_meta = new_meta || quote.getMetabucket().getData();
        for (var key in new_meta) {
            update['meta.' + key] = new_meta[key];
        }
        this.mergeData(quote, update, success, failure, options);
    };
    /**
     * Saves the quote lock state to the database
     *
     * @param quote   - the quote to save
     * @param success - function to call on success
     * @param failure - function to call if save fails
     */
    MongoServerDao.prototype.saveQuoteLockState = function (quote, success, failure) {
        if (success === void 0) { success = function () { }; }
        if (failure === void 0) { failure = function () { }; }
        // lock state is saved by default
        return this.saveQuote(quote, success, failure, {});
    };
    /**
     * Pulls quote data from the database
     *
     * @param quote_id - id of quote
     */
    MongoServerDao.prototype.pullQuote = function (quote_id) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            _this._quote_collection
                .find({ id: quote_id })
                .limit(1)
                .project(_this.PULL_FIELDS)
                .next(function (err, data) {
                if (err) {
                    return reject(err);
                }
                // was the quote found?
                if (!data) {
                    return resolve(null);
                }
                // return the quote data
                resolve(data);
            });
        });
    };
    /**
     * Returns the minimum allowable quote id
     *
     * @param callback - function to call with minimum quote id
     */
    MongoServerDao.prototype.getMinQuoteId = function (callback) {
        // just in case it's asynchronous later on
        callback.call(this, this.SEQ_QUOTE_ID_DEFAULT);
        return this;
    };
    /**
     * Returns the current highest quote id
     *
     * @param callback - function to call with current highest quote id
     */
    MongoServerDao.prototype.getMaxQuoteId = function (callback) {
        this._seq_collection
            .find({ _id: this.SEQ_QUOTE_ID })
            .limit(1)
            .next(function (err, data) {
            if (!data || err) {
                callback(0);
                return;
            }
            // return the max quote id
            callback(data.val);
        });
    };
    /**
     * Returns the next available quote id
     *
     * @param callback - function to call with next available quote id
     */
    MongoServerDao.prototype.getNextQuoteId = function () {
        var _this = this;
        return new Promise(function (accept, reject) {
            _this._seq_collection.findOneAndUpdate({ _id: _this.SEQ_QUOTE_ID }, { $inc: { val: 1 } }, { returnOriginal: false, sort: { val: -1 } }, function (err, result) {
                if (err || !result.value) {
                    _this.emit('seqError', err);
                    reject(err);
                    return;
                }
                // return the new id
                accept(result.value.val);
            });
        });
    };
    /**
     * Create a new revision with the provided quote data
     *
     * The revision will contain the whole the quote. If space is a concern, we
     * can (in the future) calculate a delta instead (Mike recommends the Git
     * model of storing the deltas in previous revisions and the whole of the
     * bucket in the most recently created revision).
     *
     * @param quote    - the quote to create a revision with
     * @param callback - function to call on error
     */
    MongoServerDao.prototype.createRevision = function (quote, callback) {
        this._quote_collection.updateOne({ id: quote.getId() }, { $push: { revisions: { data: quote.getBucket().getData() } } }, { upsert: true }, callback);
    };
    /**
     * Get a quote revision by its revision id
     *
     * @param quote    - the quote
     * @param revid    - the revision id
     * @param callback - a function to call with results
     */
    MongoServerDao.prototype.getRevision = function (quote, revid, callback) {
        this._quote_collection
            .find({ id: quote.getId() })
            .limit(1)
            .next(function (err, data) {
            // was the quote found?
            if (err || !data || data.revisions.length < +revid + 1) {
                callback(null);
                return;
            }
            // return the quote data
            callback(data.revisions[+revid]);
        });
    };
    /**
     * Set worksheet data
     *
     * @param qid      - The quote id
     * @param data     - worksheet data
     * @param callback - a function to call on error
     */
    MongoServerDao.prototype.setWorksheets = function (qid, data, callback) {
        this._quote_collection.updateOne({ id: qid }, { $set: { worksheets: { data: data } } }, { upsert: true }, callback);
    };
    /**
     * Retrieve worksheet data for a given quote id and supplier
     *
     * @param qid      - the quote id
     * @param supplier - the supplier to retrieve the worksheet for
     * @param index    - the worksheet index
     *
     * @return Promise with worksheet data
     */
    MongoServerDao.prototype.getWorksheet = function (qid, supplier, index) {
        var _this = this;
        return new Promise(function (resolve, reject) {
            _this._quote_collection
                .find({ id: qid })
                .limit(1)
                .project({ 'worksheets.data': 1 })
                .next(function (err, result) {
                if (err) {
                    reject(err);
                }
                // was the quote found?
                if (!result ||
                    !result.worksheets ||
                    !result.worksheets.data ||
                    !result.worksheets.data[supplier]) {
                    reject('Worksheet data not found');
                    return;
                }
                resolve({
                    data: result.worksheets.data[supplier][index],
                });
            });
        });
    };
    return MongoServerDao;
}(events_1.EventEmitter));
exports.MongoServerDao = MongoServerDao;
//# sourceMappingURL=data:application/json;base64, 