Source: liveview.js

/**
 * @namespace LiveView
 */
;(function(window, $, undefined){
	'use strict';
	var tmpPrototype;

	/** @alias LiveView.prototype */
	window.LiveView = {
		Table: Table,
		Query: Query,
		QueryProperties: QueryProperties,
		Schema: Schema,
		Field: Field,
		Tuple: Tuple,
		Error: Error,
		_internal: {}
	};

	/**
	 * Defines the properties of a LiveView Table. Properties are read-only.
	 * @class Table
	 * @memberOf LiveView
	 * @param {String} name The name of the table.
	 * @param {String} group The group the table belongs to.
	 * @param {String} shortDescription A short description of the table. Ideal for length-limited fields.
	 * @param {String} description A full description of the table.
	 * @param {boolean} isEnabled A flag indicating whether or not the table is enabled.
	 * @param {String} queryLanguages A CSV string of supported query languages for this table.
	 * @param {Array} keys Field names of key fields
	 * @param {LiveView.Schema} schema The Schema that defines the table fields.
	 */
	function Table(name, group, shortDescription, description, isEnabled, queryLanguages, keys, schema){
		if(!(this instanceof Table)){
			return new Table(name, group, shortDescription, description, isEnabled, queryLanguages, keys, schema);
		}
		/**
		 * The name of the table.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {String} name
		 */
		this.name = name;
		/**
		 * The group the table belongs to.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {String} group
		 */
		this.group = group;
		/**
		 * A short description of the table. Ideal for length-limited fields.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {String} shortDescription
		 */
		this.shortDescription = shortDescription;
		/**
		 * A full description of the table.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {String} description
		 */
		this.description = description;
		/**
		 * A flag indicating whether or not the table is enabled.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {boolean} isEnabled
		 */
		this.isEnabled = isEnabled;
		/**
		 * A CSV string of supported query languages for this table.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {String} queryLanguages
		 */
		this.queryLanguages = queryLanguages;
		/**
		 * An array of string field names identifying the table's key fields.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {Array} keys
		 */
		this.keys = keys;
		/**
		 * The {@link LiveView.Schema|Schema} for the table.
		 * @memberOf LiveView.Table
		 * @instance
		 * @member {LiveView.Schema} schema
		 */
		this.schema = schema;
	}
	/** @alias LiveView.Table.prototype */
	Table.prototype = {
		constructor: Table
	};

	/**
	 * Object that represents a LiveView query. This object is used as a parameter when calling
	 * {@link LiveView.Connection.execute|execute} or {@link LiveView.Connection.subscribe|subscribe}. It stores the
	 * query string (in parameterized format) as well as the parameters to apply when invoking execute or subscribe.
	 * @class Query
	 * @memberOf LiveView
	 * @param {String} query The optionally-parametrized query string
	 * @param {Object} [parameters] A key-value map of parameters to use to compute the query string, if query is a parametrized string. Users are essentially free to define the key value used to identify a parameter as they wish. The query string is generated using regular expression substitution, so avoidance of regular expression special characters is recommended as it will likely cause unexpected behavior. The '@' character as a parameter prefix works well (e.g. <span style="lvCode">{'@priceMin':100, '@lastUpdated':1415230518223}</span>).
	 * @param {boolean} [includeInternal] A flag indicating whether or not to include internal fields in (non-aggregate) query results.
	 */
	function Query(query, parameters, includeInternal){
		if(!(this instanceof Query)){
			return new Query(query, parameters, includeInternal);
		}
		/**
		 * The query string in parametrized form. For example: <span class="lvCode">"Select * From ItemsSales Where lastSoldPrice > @minPrice"</span>
		 * @memberOf LiveView.Query
		 * @instance
		 * @member {String} query
		 */
		this.query = query;
		/**
		 * A key-value map of parameters to use to compute the query string, if query is a parametrized string. Users are essentially free to define the key value used to identify a parameter as they wish. The query string is generated using regular expression substitution, so avoidance of regular expression special characters is recommended as it will likely cause unexpected behavior. The '@' character as a parameter prefix works well (e.g. <span style="lvCode">{'@priceMin':100, '@lastUpdated':1415230518223}</span>).
		 * @memberOf LiveView.Query
		 * @instance
		 * @member {Object} parameters
		 */
		this.parameters = parameters;
		/**
		 * A flag indicating whether or not to include internal fields in (non-aggregate) query results.
		 * @memberOf LiveView.Query
		 * @instance
		 * @member {boolean} includeInternal
		 */
		this.includeInternal = includeInternal;
	}
	/** @alias LiveView.Query.prototype */
	Query.prototype = {
		constructor: Query,
		/**
		 * Applies parameters to the query and returns the resulting query string. If this is not a parametrized query,
		 * this will be equivalent to the query field.
		 * @function
		 * @memberOf LiveView.Query.prototype
		 * @returns {String} -- The parameter-applied query string
		 */
		getQueryString: function(){
			var key,
				escapedKey,
				queryString = this.query;
			for(key in this.parameters){
				if(this.parameters.hasOwnProperty(key)){
					//we need to escape the key which could contain special characters, we need to treat them as literals
					escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
					queryString = queryString.replace(new RegExp(escapedKey + '\\b', 'g'), this.parameters[key]);
				}
			}
			return queryString;
		},
		/**
		 * Formats the query string as required by the LiveView server.
		 * @function
		 * @memberOf LiveView.Query.prototype
		 * @returns {String} -- The parameter-applied, server-formatted query string
		 */
		serialize: function(){
			return this.getQueryString.call(this).replace(/^delete\sfrom\s/i ,'SELECT * FROM ').replace(/^delete/i,'SELECT * FROM ');
		}
	};

	/**
	 * Stores detailed information about a LiveView query.
	 * @class QueryProperties
	 * @memberOf LiveView
	 * @param {Object} [initialValues] An object containing initial values for the QueryProperties object's properties.
	 * The property names defined in the initialValues object should match those of the corresponding QueryProperties
	 * member properties.
	 */
	function QueryProperties(initialValues){
		initialValues = initialValues || {};
		/**
		 * Flag indicating whether or not the query was configured to include LiveView-internal field values.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {boolean} includeInternal
		 * @default false
		 */
		this.includeInternal = initialValues.includeInternal === true;
		/**
		 * Flag indicating whether or not the query is an aggregate query.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {boolean} isAggregate
		 * @default false
		 */
		this.isAggregate = initialValues.isAggregate === true;
		/**
		 * If a limit was specified in the query, this is the numerical value of that limit. For example, if the query
		 * was 'SELECT * FROM ItemsSales LIMIT 30', then the limit value would be 30. If no limit was specified, then
		 * the value will be -1;
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {Number} limit
		 * @default -1
		 */
		this.limit = isNaN(initialValues.limit) ? -1:initialValues.limit;
		/**
		 * An array of objects that define the ORDER BY clause of the query. The order of elements in the array
		 * determines the priority of ordering (i.e. orderBy[0] is of the highest priority). The elements in the array
		 * contain two properties: <strong>fieldName</strong> and <strong>direction</strong>. The
		 * <strong>fieldName</strong> property is the string name that identifies the ORDER BY field. The
		 * <strong>direction</strong> property indicates is a string that indicates what direction to order the values
		 * ('ASC' for ascending order and 'DESC' for descending order).
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {Array} orderBy
		 * @default []
		 */
		this.orderBy = initialValues.orderBy || [];
		/**
		 * The predicate or set of conditions appearing in the query's WHERE clause.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {String} predicate
		 * @default null
		 */
		this.predicate = initialValues.predicate || null;
		/**
		 * If a time-delay modifier was added to the WHEN clause, predicateDelay is the value of the delay in
		 * milliseconds. For example, if the query was 'SELECT * FROM ItemsSales WHERE price > 100 FOR 1000', then
		 * predicateDelay would be 1000). If no time-delay modifier was specified, predicateDelay will be -1.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {Number} predicateDelay
		 * @default 0
		 */
		this.predicateDelay = isNaN(initialValues.predicateDelay) ? 0:initialValues.predicateDelay;
		/**
		 * The projection of the query (i.e. those fields appearing in the query's FROM clause). Stored in CSV format.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {String} projection
		 * @default null
		 */
		this.projection = initialValues.projection || null;
		/**
		 * One of ['SNAPSHOT', 'CONTINUOUS', 'SNAPSHOT_AND_CONTINUOUS', 'DELETE']
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {String} queryType
		 * @default null
		 */
		this.queryType = initialValues.queryType || null;
		/**
		 * The {@link LiveView.Schema|Schema} of the parsed query.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {Object} schema
		 * @default null
		 */
		this.schema = initialValues.schema || null;
		/**
		 * The name of the table against which the query will be performed.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {String} table
		 * @default null
		 */
		this.table = initialValues.table || null;
		/**
		 * If a time-window is specified in the query, this object will contain three properties: <strong>field</strong>,
		 * <strong>begin</strong>, and <strong>end</strong>. The <strong>field</strong> property is the string name of
		 * the field specified in the WHEN clause. The <strong>begin</strong> property defines the beginning of the
		 * time-window. The <strong>end</strong> property defines the end of the time-window. For example, if the query
		 * is 'SELECT * FROM ItemsSales WHEN transactionTime BETWEEN now()-seconds(30) AND now()', the field would be
		 * 'transactionTime', the begin would be 'now()-seconds(30)' and the end would be 'now()'.
		 * @memberOf LiveView.QueryProperties
		 * @instance
		 * @member {Object} when
		 * @default null
		 */
		this.when = initialValues.when || null;
	}
	//QueryProperties.fromServerQueryModel - used for internal parsing
	QueryProperties.fromDescribeQueryResult = function(describeQueryResult){
		var queryProps = buildPropsFromServerModel(describeQueryResult.alert_query_config);
		queryProps.schema = parseSchemaString(describeQueryResult.schema_xml.schema);
		return queryProps;

		function buildPropsFromServerModel(serverQueryModel){
			serverQueryModel = serverQueryModel || {};
			return new QueryProperties({
				includeInternal: serverQueryModel.include_internal || false,
				isAggregate: serverQueryModel.is_aggregate || false,
				limit: isNaN(serverQueryModel.limit) ? 0:serverQueryModel.limit,
				orderBy: (serverQueryModel.order_by && serverQueryModel.order_by.directions instanceof Array) ? serverQueryModel.order_by.directions:[],
				predicate: serverQueryModel.predicate || '',
				predicateDelay: isNaN(serverQueryModel.predicate_delay_in_millis) ? 0:serverQueryModel.predicate_delay_in_millis,
				projection: serverQueryModel.projection || '',
				queryType: serverQueryModel.query_type || '',
				table: serverQueryModel.table,
				when: (!serverQueryModel.time_window ? {}: {
					field: serverQueryModel.time_window.timstamp_field,
					begin: serverQueryModel.time_window.window_start_expr,
					end: serverQueryModel.time_window.window_end_expr
				})
			});
		}

		function parseSchemaString(schemaStr){
			var schemaObj, fields = [], i;
			schemaObj = JSON.parse(schemaStr);
			for(i = 0; i < schemaObj.fields.length; i++){
				fields.push(new Field(
					schemaObj.fields[i].name,
					schemaObj.fields[i].description || '',
					schemaObj.fields[i].type.type,
					schemaObj.fields[i].schema || null
				));
			}
			return new Schema(schemaObj.name || '', fields);
		}
	};

	/**
	 * A LiveView Schema is used for Queries and Tables to define their respective schemas.
	 * @class Schema
	 * @memberOf LiveView
	 * @param {String} name The name of the schema
	 * @param {Array} fields Array of {@link LiveView.Field|fields} that compose the schema.
	 */
	function Schema(name, fields){
		var i;
		if(!(this instanceof Schema)){
			return new Schema(name, fields);
		}
		/**
		 * Name of the Schema.
		 * @memberOf LiveView.Schema
		 * @instance
		 * @member {String} name
		 */
		this.name = name;
		/**
		 * The fields that define this schema as an array. The array preserves field order as received from the
		 * LiveView server.
		 * @memberOf LiveView.Schema
		 * @instance
		 * @member {Array} fields
		 */
		this.fields = fields;
		/**
		 * The fields that define this schema mapped by field name. The order of fields in the field map may not be
		 * consistent with the actual schema ordering. The fields array maintains fields in the correct order.
		 * @memberOf LiveView.Schema
		 * @instance
		 * @member {Object<String, LiveView.Field>} fieldsMap
		 */
		this.fieldsMap = {};

		for(i = 0; i < fields.length; i++){
			this.fieldsMap[fields[i].name] = fields[i];
		}
	}
	/** @alias LiveView.Schema.prototype */
	Schema.prototype = {
		constructor: Schema
	};

	/**
	 * Defines the properties of a LiveView Field.
	 * @class Field
	 * @memberOf LiveView
	 * @param {String} name The field name. This is used as the unique identifier for this field.
	 * @param {String} description A description of the field.
	 * @param {String} type The type of the field (e.g "string", "int", etc).
	 * @param {LiveView.Schema} [schema] The schema of this field if its type is "tuple".
	 */
	function Field(name, description, type, schema){
		if(!(this instanceof Field)){
			return new Field(name, description, type, schema);
		}
		/**
		 * The field name. This is used as the unique identifier for this field.
		 * @memberOf LiveView.Field
		 * @instance
		 * @member {String} name
		 */
		this.name = name;
		/**
		 * A full length description of the field.
		 * @memberOf LiveView.Field
		 * @instance
		 * @member {String} description
		 */
		this.description = description;
		/**
		 * The type of the field (e.g "string", "int", etc).
		 * @memberOf LiveView.Field
		 * @instance
		 * @member {String} type
		 */
		this.type = type;
		/**
		 * The schema of this field if its type is "tuple".
		 * @memberOf LiveView.Field
		 * @instance
		 * @member {LiveView.Schema} schema
		 */
		this.schema = schema;
	}
	/** @alias LiveView.Field.prototype */
	Field.prototype = {
		constructor: Field
	};

	/**
	 *
	 * @class Tuple
	 * @memberOf LiveView
	 * @param {Number} id The tuple's unique ID
	 * @param {Object} fieldMap Map of fieldName -> fieldValue
	 */
	function Tuple(id, fieldMap){
		if(!(this instanceof Tuple)){
			return new Tuple(id, fieldMap);
		}
		/**
		 * The tuple's unique ID
		 * @memberOf LiveView.Tuple
		 * @instance
		 * @member {Number} id
		 */
		this.id = id;
		/**
		 * Map of fieldName -> fieldValue for all fields in the tuple. Note that the order of fields in the map will not
		 * necessarily correspond to the order of fields in the corresponding schema. One should refer to the
		 * {@link LiveView.Schema#fields|fields} array of the corresponding schema to obtain the correct ordering of
		 * fields.
		 * @memberOf LiveView.Tuple
		 * @instance
		 * @member {Object} fieldMap
		 */
		this.fieldMap = fieldMap;
	}
	Tuple.prototype = {
		constructor: Tuple
	};

	/**
	 * LiveView error object that will be passed as an argument to error handling callbacks and promise rejection
	 * handlers. LiveView.Error inherits from window.Error if available.
	 * @class Error
	 * @memberOf LiveView
	 * @param {Number} code The error code that identifies the error.
	 * @param {String} detail A detailed message about what happened.
	 * @param {String} message A brief description summarizing the error.
	 * @constructor
	 */
	function Error(code, detail, message){
		var envErr;
		if(!(this instanceof Error)){
			return new Error(code, detail, message);
		}

		if(typeof(code) === 'object'){
			detail = code.detail || '';
			message = code.message || '';
			code = code.code || null;
		}

		//Add useful error info to our LiveView.Error from the native Error object (some values won't be set)
		if(typeof(window.Error) === 'function'){
			envErr = new window.Error(message);
			/**
			 * The error stack trace if available in the current browser.
			 * @memberOf LiveView.Error
			 * @instance
			 * @member {String} stack
			 */
			this.stack = envErr.stack;
		}

		//TODO: Enumerate error codes and update Error creations to use appropriate code
		/**
		 * The error code that identifies the error.
		 * @memberOf LiveView.Error
		 * @instance
		 * @member {Number} code
		 */
		this.code = code;
		/**
		 * A detailed message about what happened.
		 * @memberOf LiveView.Error
		 * @instance
		 * @member {String} detail
		 */
		this.detail = detail;

		/**
		 * A brief description summarizing the error.
		 * @memberOf LiveView.Error
		 * @instance
		 * @member {String} message
		 */
		this.message = message;
	}

	tmpPrototype = {
		constructor: Error,
		toString: function(){
			return JSON.stringify(this);
		}
	};

	if(window.Error && typeof(window.Error.prototype) === 'object'){
		Error.prototype = jQuery.extend(Object.create(window.Error.prototype), tmpPrototype); //so LiveView.Error instance of Error is true
	}
	else{
		Error.prototype = tmpPrototype;
	}

}(window, jQuery));