leancloud-storage
Version:
LeanCloud JavaScript SDK.
917 lines (831 loc) • 31.8 kB
JavaScript
'use strict';
var _ = require('underscore');
var AVError = require('./error');
var AVRequest = require('./request').request;
var _require = require('./utils'),
ensureArray = _require.ensureArray;
var requires = function requires(value, message) {
if (value === undefined) {
throw new Error(message);
}
};
// AV.Query is a way to create a list of AV.Objects.
module.exports = function (AV) {
/**
* Creates a new AV.Query for the given AV.Object subclass.
* @param {Class|String} objectClass An instance of a subclass of AV.Object, or a AV className string.
* @class
*
* <p>AV.Query defines a query that is used to fetch AV.Objects. The
* most common use case is finding all objects that match a query through the
* <code>find</code> method. For example, this sample code fetches all objects
* of class <code>MyClass</code>. It calls a different function depending on
* whether the fetch succeeded or not.
*
* <pre>
* var query = new AV.Query(MyClass);
* query.find().then(function(results) {
* // results is an array of AV.Object.
* }, function(error) {
* // error is an instance of AVError.
* });</pre></p>
*
* <p>An AV.Query can also be used to retrieve a single object whose id is
* known, through the get method. For example, this sample code fetches an
* object of class <code>MyClass</code> and id <code>myId</code>. It calls a
* different function depending on whether the fetch succeeded or not.
*
* <pre>
* var query = new AV.Query(MyClass);
* query.get(myId).then(function(object) {
* // object is an instance of AV.Object.
* }, function(error) {
* // error is an instance of AVError.
* });</pre></p>
*
* <p>An AV.Query can also be used to count the number of objects that match
* the query without retrieving all of those objects. For example, this
* sample code counts the number of objects of the class <code>MyClass</code>
* <pre>
* var query = new AV.Query(MyClass);
* query.count().then(function(number) {
* // There are number instances of MyClass.
* }, function(error) {
* // error is an instance of AVError.
* });</pre></p>
*/
AV.Query = function (objectClass) {
if (_.isString(objectClass)) {
objectClass = AV.Object._getSubclass(objectClass);
}
this.objectClass = objectClass;
this.className = objectClass.prototype.className;
this._where = {};
this._include = [];
this._select = [];
this._limit = -1; // negative limit means, do not send a limit
this._skip = 0;
this._extraOptions = {};
};
/**
* Constructs a AV.Query that is the OR of the passed in queries. For
* example:
* <pre>var compoundQuery = AV.Query.or(query1, query2, query3);</pre>
*
* will create a compoundQuery that is an or of the query1, query2, and
* query3.
* @param {...AV.Query} var_args The list of queries to OR.
* @return {AV.Query} The query that is the OR of the passed in queries.
*/
AV.Query.or = function () {
var queries = _.toArray(arguments);
var className = null;
AV._arrayEach(queries, function (q) {
if (_.isNull(className)) {
className = q.className;
}
if (className !== q.className) {
throw "All queries must be for the same class";
}
});
var query = new AV.Query(className);
query._orQuery(queries);
return query;
};
/**
* Constructs a AV.Query that is the AND of the passed in queries. For
* example:
* <pre>var compoundQuery = AV.Query.and(query1, query2, query3);</pre>
*
* will create a compoundQuery that is an 'and' of the query1, query2, and
* query3.
* @param {...AV.Query} var_args The list of queries to AND.
* @return {AV.Query} The query that is the AND of the passed in queries.
*/
AV.Query.and = function () {
var queries = _.toArray(arguments);
var className = null;
AV._arrayEach(queries, function (q) {
if (_.isNull(className)) {
className = q.className;
}
if (className !== q.className) {
throw "All queries must be for the same class";
}
});
var query = new AV.Query(className);
query._andQuery(queries);
return query;
};
/**
* Retrieves a list of AVObjects that satisfy the CQL.
* CQL syntax please see {@link https://leancloud.cn/docs/cql_guide.html CQL Guide}.
*
* @param {String} cql A CQL string, see {@link https://leancloud.cn/docs/cql_guide.html CQL Guide}.
* @param {Array} pvalues An array contains placeholder values.
* @param {AuthOptions} options
* @return {Promise} A promise that is resolved with the results when
* the query completes.
*/
AV.Query.doCloudQuery = function (cql, pvalues, options) {
var params = { cql: cql };
if (_.isArray(pvalues)) {
params.pvalues = pvalues;
} else {
options = pvalues;
}
var request = AVRequest('cloudQuery', null, null, 'GET', params, options);
return request.then(function (response) {
//query to process results.
var query = new AV.Query(response.className);
var results = _.map(response.results, function (json) {
var obj = query._newObject(response);
if (obj._finishFetch) {
obj._finishFetch(query._processResult(json), true);
}
return obj;
});
return {
results: results,
count: response.count,
className: response.className
};
});
};
AV.Query._extend = AV._extend;
AV.Query.prototype = {
//hook to iterate result. Added by dennis<xzhuang@avoscloud.com>.
_processResult: function _processResult(obj) {
return obj;
},
/**
* Constructs an AV.Object whose id is already known by fetching data from
* the server.
*
* @param {String} objectId The id of the object to be fetched.
* @param {AuthOptions} options
* @return {Promise.<AV.Object>}
*/
get: function get(objectId, options) {
if (!objectId) {
var errorObject = new AVError(AVError.OBJECT_NOT_FOUND, "Object not found.");
throw errorObject;
}
var self = this;
var obj = self._newObject();
obj.id = objectId;
var queryJSON = self.toJSON();
var fetchOptions = {};
if (queryJSON.keys) fetchOptions.keys = queryJSON.keys;
if (queryJSON.include) fetchOptions.include = queryJSON.include;
return obj.fetch(fetchOptions, options);
},
/**
* Returns a JSON representation of this query.
* @return {Object}
*/
toJSON: function toJSON() {
var params = {
where: this._where
};
if (this._include.length > 0) {
params.include = this._include.join(",");
}
if (this._select.length > 0) {
params.keys = this._select.join(",");
}
if (this._limit >= 0) {
params.limit = this._limit;
}
if (this._skip > 0) {
params.skip = this._skip;
}
if (this._order !== undefined) {
params.order = this._order;
}
AV._objectEach(this._extraOptions, function (v, k) {
params[k] = v;
});
return params;
},
_newObject: function _newObject(response) {
var obj;
if (response && response.className) {
obj = new AV.Object(response.className);
} else {
obj = new this.objectClass();
}
return obj;
},
_createRequest: function _createRequest(params, options) {
return AVRequest('classes', this.className, null, "GET", params || this.toJSON(), options);
},
/**
* Retrieves a list of AVObjects that satisfy this query.
*
* @param {AuthOptions} options
* @return {Promise} A promise that is resolved with the results when
* the query completes.
*/
find: function find(options) {
var self = this;
var request = this._createRequest(null, options);
return request.then(function (response) {
return _.map(response.results, function (json) {
var obj = self._newObject(response);
if (obj._finishFetch) {
obj._finishFetch(self._processResult(json), true);
}
return obj;
});
});
},
/**
* Delete objects retrieved by this query.
* @param {AuthOptions} options
* @return {Promise} A promise that is fulfilled when the save
* completes.
*/
destroyAll: function destroyAll(options) {
var self = this;
return self.find(options).then(function (objects) {
return AV.Object.destroyAll(objects);
});
},
/**
* Counts the number of objects that match this query.
*
* @param {AuthOptions} options
* @return {Promise} A promise that is resolved with the count when
* the query completes.
*/
count: function count(options) {
var params = this.toJSON();
params.limit = 0;
params.count = 1;
var request = this._createRequest(params, options);
return request.then(function (response) {
return response.count;
});
},
/**
* Retrieves at most one AV.Object that satisfies this query.
*
* @param {AuthOptions} options
* @return {Promise} A promise that is resolved with the object when
* the query completes.
*/
first: function first(options) {
var self = this;
var params = this.toJSON();
params.limit = 1;
var request = this._createRequest(params, options);
return request.then(function (response) {
return _.map(response.results, function (json) {
var obj = self._newObject();
if (obj._finishFetch) {
obj._finishFetch(self._processResult(json), true);
}
return obj;
})[0];
});
},
/**
* Sets the number of results to skip before returning any results.
* This is useful for pagination.
* Default is to skip zero results.
* @param {Number} n the number of results to skip.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
skip: function skip(n) {
requires(n, 'undefined is not a valid skip value');
this._skip = n;
return this;
},
/**
* Sets the limit of the number of results to return. The default limit is
* 100, with a maximum of 1000 results being returned at a time.
* @param {Number} n the number of results to limit to.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
limit: function limit(n) {
requires(n, 'undefined is not a valid limit value');
this._limit = n;
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be equal to the provided value.
* @param {String} key The key to check.
* @param value The value that the AV.Object must contain.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
equalTo: function equalTo(key, value) {
requires(key, 'undefined is not a valid key');
requires(value, 'undefined is not a valid value');
this._where[key] = AV._encode(value);
return this;
},
/**
* Helper for condition queries
* @private
*/
_addCondition: function _addCondition(key, condition, value) {
requires(key, 'undefined is not a valid condition key');
requires(condition, 'undefined is not a valid condition');
requires(value, 'undefined is not a valid condition value');
// Check if we already have a condition
if (!this._where[key]) {
this._where[key] = {};
}
this._where[key][condition] = AV._encode(value);
return this;
},
/**
* Add a constraint to the query that requires a particular
* <strong>array</strong> key's length to be equal to the provided value.
* @param {String} key The array key to check.
* @param value The length value.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
sizeEqualTo: function sizeEqualTo(key, value) {
this._addCondition(key, "$size", value);
},
/**
* Add a constraint to the query that requires a particular key's value to
* be not equal to the provided value.
* @param {String} key The key to check.
* @param value The value that must not be equalled.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
notEqualTo: function notEqualTo(key, value) {
this._addCondition(key, "$ne", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be less than the provided value.
* @param {String} key The key to check.
* @param value The value that provides an upper bound.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
lessThan: function lessThan(key, value) {
this._addCondition(key, "$lt", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be greater than the provided value.
* @param {String} key The key to check.
* @param value The value that provides an lower bound.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
greaterThan: function greaterThan(key, value) {
this._addCondition(key, "$gt", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be less than or equal to the provided value.
* @param {String} key The key to check.
* @param value The value that provides an upper bound.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
lessThanOrEqualTo: function lessThanOrEqualTo(key, value) {
this._addCondition(key, "$lte", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be greater than or equal to the provided value.
* @param {String} key The key to check.
* @param value The value that provides an lower bound.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
greaterThanOrEqualTo: function greaterThanOrEqualTo(key, value) {
this._addCondition(key, "$gte", value);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* be contained in the provided list of values.
* @param {String} key The key to check.
* @param {Array} values The values that will match.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
containedIn: function containedIn(key, values) {
this._addCondition(key, "$in", values);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* not be contained in the provided list of values.
* @param {String} key The key to check.
* @param {Array} values The values that will not match.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
notContainedIn: function notContainedIn(key, values) {
this._addCondition(key, "$nin", values);
return this;
},
/**
* Add a constraint to the query that requires a particular key's value to
* contain each one of the provided list of values.
* @param {String} key The key to check. This key's value must be an array.
* @param {Array} values The values that will match.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
containsAll: function containsAll(key, values) {
this._addCondition(key, "$all", values);
return this;
},
/**
* Add a constraint for finding objects that contain the given key.
* @param {String} key The key that should exist.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
exists: function exists(key) {
this._addCondition(key, "$exists", true);
return this;
},
/**
* Add a constraint for finding objects that do not contain a given key.
* @param {String} key The key that should not exist
* @return {AV.Query} Returns the query, so you can chain this call.
*/
doesNotExist: function doesNotExist(key) {
this._addCondition(key, "$exists", false);
return this;
},
/**
* Add a regular expression constraint for finding string values that match
* the provided regular expression.
* This may be slow for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {RegExp} regex The regular expression pattern to match.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
matches: function matches(key, regex, modifiers) {
this._addCondition(key, "$regex", regex);
if (!modifiers) {
modifiers = "";
}
// Javascript regex options support mig as inline options but store them
// as properties of the object. We support mi & should migrate them to
// modifiers
if (regex.ignoreCase) {
modifiers += 'i';
}
if (regex.multiline) {
modifiers += 'm';
}
if (modifiers && modifiers.length) {
this._addCondition(key, "$options", modifiers);
}
return this;
},
/**
* Add a constraint that requires that a key's value matches a AV.Query
* constraint.
* @param {String} key The key that the contains the object to match the
* query.
* @param {AV.Query} query The query that should match.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
matchesQuery: function matchesQuery(key, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$inQuery", queryJSON);
return this;
},
/**
* Add a constraint that requires that a key's value not matches a
* AV.Query constraint.
* @param {String} key The key that the contains the object to match the
* query.
* @param {AV.Query} query The query that should not match.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
doesNotMatchQuery: function doesNotMatchQuery(key, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$notInQuery", queryJSON);
return this;
},
/**
* Add a constraint that requires that a key's value matches a value in
* an object returned by a different AV.Query.
* @param {String} key The key that contains the value that is being
* matched.
* @param {String} queryKey The key in the objects returned by the query to
* match against.
* @param {AV.Query} query The query to run.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
matchesKeyInQuery: function matchesKeyInQuery(key, queryKey, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$select", { key: queryKey, query: queryJSON });
return this;
},
/**
* Add a constraint that requires that a key's value not match a value in
* an object returned by a different AV.Query.
* @param {String} key The key that contains the value that is being
* excluded.
* @param {String} queryKey The key in the objects returned by the query to
* match against.
* @param {AV.Query} query The query to run.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
doesNotMatchKeyInQuery: function doesNotMatchKeyInQuery(key, queryKey, query) {
var queryJSON = query.toJSON();
queryJSON.className = query.className;
this._addCondition(key, "$dontSelect", { key: queryKey, query: queryJSON });
return this;
},
/**
* Add constraint that at least one of the passed in queries matches.
* @param {Array} queries
* @return {AV.Query} Returns the query, so you can chain this call.
* @private
*/
_orQuery: function _orQuery(queries) {
var queryJSON = _.map(queries, function (q) {
return q.toJSON().where;
});
this._where.$or = queryJSON;
return this;
},
/**
* Add constraint that both of the passed in queries matches.
* @param {Array} queries
* @return {AV.Query} Returns the query, so you can chain this call.
* @private
*/
_andQuery: function _andQuery(queries) {
var queryJSON = _.map(queries, function (q) {
return q.toJSON().where;
});
this._where.$and = queryJSON;
return this;
},
/**
* Converts a string into a regex that matches it.
* Surrounding with \Q .. \E does this, we just need to escape \E's in
* the text separately.
* @private
*/
_quote: function _quote(s) {
return "\\Q" + s.replace("\\E", "\\E\\\\E\\Q") + "\\E";
},
/**
* Add a constraint for finding string values that contain a provided
* string. This may be slow for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {String} substring The substring that the value must contain.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
contains: function contains(key, value) {
this._addCondition(key, "$regex", this._quote(value));
return this;
},
/**
* Add a constraint for finding string values that start with a provided
* string. This query will use the backend index, so it will be fast even
* for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {String} prefix The substring that the value must start with.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
startsWith: function startsWith(key, value) {
this._addCondition(key, "$regex", "^" + this._quote(value));
return this;
},
/**
* Add a constraint for finding string values that end with a provided
* string. This will be slow for large datasets.
* @param {String} key The key that the string to match is stored in.
* @param {String} suffix The substring that the value must end with.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
endsWith: function endsWith(key, value) {
this._addCondition(key, "$regex", this._quote(value) + "$");
return this;
},
/**
* Sorts the results in ascending order by the given key.
*
* @param {String} key The key to order by.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
ascending: function ascending(key) {
requires(key, 'undefined is not a valid key');
this._order = key;
return this;
},
/**
* Also sorts the results in ascending order by the given key. The previous sort keys have
* precedence over this key.
*
* @param {String} key The key to order by
* @return {AV.Query} Returns the query so you can chain this call.
*/
addAscending: function addAscending(key) {
requires(key, 'undefined is not a valid key');
if (this._order) this._order += ',' + key;else this._order = key;
return this;
},
/**
* Sorts the results in descending order by the given key.
*
* @param {String} key The key to order by.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
descending: function descending(key) {
requires(key, 'undefined is not a valid key');
this._order = "-" + key;
return this;
},
/**
* Also sorts the results in descending order by the given key. The previous sort keys have
* precedence over this key.
*
* @param {String} key The key to order by
* @return {AV.Query} Returns the query so you can chain this call.
*/
addDescending: function addDescending(key) {
requires(key, 'undefined is not a valid key');
if (this._order) this._order += ',-' + key;else this._order = '-' + key;
return this;
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given.
* @param {String} key The key that the AV.GeoPoint is stored in.
* @param {AV.GeoPoint} point The reference AV.GeoPoint that is used.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
near: function near(key, point) {
if (!(point instanceof AV.GeoPoint)) {
// Try to cast it to a GeoPoint, so that near("loc", [20,30]) works.
point = new AV.GeoPoint(point);
}
this._addCondition(key, "$nearSphere", point);
return this;
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given and within the maximum distance given.
* @param {String} key The key that the AV.GeoPoint is stored in.
* @param {AV.GeoPoint} point The reference AV.GeoPoint that is used.
* @param maxDistance Maximum distance (in radians) of results to return.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
withinRadians: function withinRadians(key, point, distance) {
this.near(key, point);
this._addCondition(key, "$maxDistance", distance);
return this;
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given and within the maximum distance given.
* Radius of earth used is 3958.8 miles.
* @param {String} key The key that the AV.GeoPoint is stored in.
* @param {AV.GeoPoint} point The reference AV.GeoPoint that is used.
* @param {Number} maxDistance Maximum distance (in miles) of results to
* return.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
withinMiles: function withinMiles(key, point, distance) {
return this.withinRadians(key, point, distance / 3958.8);
},
/**
* Add a proximity based constraint for finding objects with key point
* values near the point given and within the maximum distance given.
* Radius of earth used is 6371.0 kilometers.
* @param {String} key The key that the AV.GeoPoint is stored in.
* @param {AV.GeoPoint} point The reference AV.GeoPoint that is used.
* @param {Number} maxDistance Maximum distance (in kilometers) of results
* to return.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
withinKilometers: function withinKilometers(key, point, distance) {
return this.withinRadians(key, point, distance / 6371.0);
},
/**
* Add a constraint to the query that requires a particular key's
* coordinates be contained within a given rectangular geographic bounding
* box.
* @param {String} key The key to be constrained.
* @param {AV.GeoPoint} southwest
* The lower-left inclusive corner of the box.
* @param {AV.GeoPoint} northeast
* The upper-right inclusive corner of the box.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
withinGeoBox: function withinGeoBox(key, southwest, northeast) {
if (!(southwest instanceof AV.GeoPoint)) {
southwest = new AV.GeoPoint(southwest);
}
if (!(northeast instanceof AV.GeoPoint)) {
northeast = new AV.GeoPoint(northeast);
}
this._addCondition(key, '$within', { '$box': [southwest, northeast] });
return this;
},
/**
* Include nested AV.Objects for the provided key. You can use dot
* notation to specify which fields in the included object are also fetch.
* @param {String[]} keys The name of the key to include.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
include: function include(keys) {
var _this = this;
requires(keys, 'undefined is not a valid key');
_(arguments).forEach(function (keys) {
_this._include = _this._include.concat(ensureArray(keys));
});
return this;
},
/**
* Restrict the fields of the returned AV.Objects to include only the
* provided keys. If this is called multiple times, then all of the keys
* specified in each of the calls will be included.
* @param {String[]} keys The names of the keys to include.
* @return {AV.Query} Returns the query, so you can chain this call.
*/
select: function select(keys) {
var _this2 = this;
requires(keys, 'undefined is not a valid key');
_(arguments).forEach(function (keys) {
_this2._select = _this2._select.concat(ensureArray(keys));
});
return this;
},
/**
* Iterates over each result of a query, calling a callback for each one. If
* the callback returns a promise, the iteration will not continue until
* that promise has been fulfilled. If the callback returns a rejected
* promise, then iteration will stop with that error. The items are
* processed in an unspecified order. The query may not have any sort order,
* and may not use limit or skip.
* @param callback {Function} Callback that will be called with each result
* of the query.
* @return {Promise} A promise that will be fulfilled once the
* iteration has completed.
*/
each: function each(callback) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (this._order || this._skip || this._limit >= 0) {
var error = new Error("Cannot iterate on a query with sort, skip, or limit.");
return AV.Promise.reject(error);
}
var query = new AV.Query(this.objectClass);
// We can override the batch size from the options.
// This is undocumented, but useful for testing.
query._limit = options.batchSize || 100;
query._where = _.clone(this._where);
query._include = _.clone(this._include);
query.ascending('objectId');
var finished = false;
return AV.Promise._continueWhile(function () {
return !finished;
}, function () {
return query.find(options).then(function (results) {
var callbacksDone = AV.Promise.resolve();
_.each(results, function (result) {
callbacksDone = callbacksDone.then(function () {
return callback(result);
});
});
return callbacksDone.then(function () {
if (results.length >= query._limit) {
query.greaterThan("objectId", results[results.length - 1].id);
} else {
finished = true;
}
});
});
});
}
};
AV.FriendShipQuery = AV.Query._extend({
_objectClass: AV.User,
_newObject: function _newObject() {
return new AV.User();
},
_processResult: function _processResult(json) {
if (json && json[this._friendshipTag]) {
var user = json[this._friendshipTag];
if (user.__type === 'Pointer' && user.className === '_User') {
delete user.__type;
delete user.className;
}
return user;
} else {
return null;
}
}
});
};