models: move Change LDL def into a json file

This commit is contained in:
Miroslav Bajtoš 2014-10-13 11:58:14 +02:00
parent 6cbc231fba
commit 0906a6f5b3
4 changed files with 575 additions and 563 deletions

View File

@ -10,26 +10,6 @@ var PersistedModel = require('../../lib/loopback').PersistedModel
, assert = require('assert') , assert = require('assert')
, debug = require('debug')('loopback:change'); , debug = require('debug')('loopback:change');
/*!
* Properties
*/
var properties = {
id: {type: String, id: true},
rev: {type: String},
prev: {type: String},
checkpoint: {type: Number},
modelName: {type: String},
modelId: {type: String}
};
/*!
* Options
*/
var options = {
trackChanges: false
};
/** /**
* Change list entry. * Change list entry.
@ -41,45 +21,45 @@ var options = {
* @property {String} modelName Model name * @property {String} modelName Model name
* @property {String} modelId Model ID * @property {String} modelId Model ID
* *
* @class * @class Change
* @inherits {Model} * @inherits {PersistedModel}
*/ */
var Change = module.exports = PersistedModel.extend('Change', properties, options); module.exports = function(Change) {
/*! /*!
* Constants * Constants
*/ */
Change.UPDATE = 'update'; Change.UPDATE = 'update';
Change.CREATE = 'create'; Change.CREATE = 'create';
Change.DELETE = 'delete'; Change.DELETE = 'delete';
Change.UNKNOWN = 'unknown'; Change.UNKNOWN = 'unknown';
/*! /*!
* Conflict Class * Conflict Class
*/ */
Change.Conflict = Conflict; Change.Conflict = Conflict;
/*! /*!
* Setup the extended model. * Setup the extended model.
*/ */
Change.setup = function() { Change.setup = function() {
PersistedModel.setup.call(this); PersistedModel.setup.call(this);
var Change = this; var Change = this;
Change.getter.id = function() { Change.getter.id = function() {
var hasModel = this.modelName && this.modelId; var hasModel = this.modelName && this.modelId;
if(!hasModel) return null; if (!hasModel) return null;
return Change.idForModel(this.modelName, this.modelId); return Change.idForModel(this.modelName, this.modelId);
} }
} }
Change.setup(); Change.setup();
/** /**
* Track the recent change of the given modelIds. * Track the recent change of the given modelIds.
* *
* @param {String} modelName * @param {String} modelName
@ -89,22 +69,22 @@ Change.setup();
* @param {Array} changes Changes that were tracked * @param {Array} changes Changes that were tracked
*/ */
Change.rectifyModelChanges = function(modelName, modelIds, callback) { Change.rectifyModelChanges = function(modelName, modelIds, callback) {
var tasks = []; var tasks = [];
var Change = this; var Change = this;
modelIds.forEach(function(id) { modelIds.forEach(function(id) {
tasks.push(function(cb) { tasks.push(function(cb) {
Change.findOrCreateChange(modelName, id, function(err, change) { Change.findOrCreateChange(modelName, id, function(err, change) {
if(err) return Change.handleError(err, cb); if (err) return Change.handleError(err, cb);
change.rectify(cb); change.rectify(cb);
}); });
}); });
}); });
async.parallel(tasks, callback); async.parallel(tasks, callback);
} }
/** /**
* Get an identifier for a given model. * Get an identifier for a given model.
* *
* @param {String} modelName * @param {String} modelName
@ -112,11 +92,11 @@ Change.rectifyModelChanges = function(modelName, modelIds, callback) {
* @return {String} * @return {String}
*/ */
Change.idForModel = function(modelName, modelId) { Change.idForModel = function(modelName, modelId) {
return this.hash([modelName, modelId].join('-')); return this.hash([modelName, modelId].join('-'));
} }
/** /**
* Find or create a change for the given model. * Find or create a change for the given model.
* *
* @param {String} modelName * @param {String} modelName
@ -127,14 +107,14 @@ Change.idForModel = function(modelName, modelId) {
* @end * @end
*/ */
Change.findOrCreateChange = function(modelName, modelId, callback) { Change.findOrCreateChange = function(modelName, modelId, callback) {
assert(loopback.findModel(modelName), modelName + ' does not exist'); assert(loopback.findModel(modelName), modelName + ' does not exist');
var id = this.idForModel(modelName, modelId); var id = this.idForModel(modelName, modelId);
var Change = this; var Change = this;
this.findById(id, function(err, change) { this.findById(id, function(err, change) {
if(err) return callback(err); if (err) return callback(err);
if(change) { if (change) {
callback(null, change); callback(null, change);
} else { } else {
var ch = new Change({ var ch = new Change({
@ -146,9 +126,9 @@ Change.findOrCreateChange = function(modelName, modelId, callback) {
ch.save(callback); ch.save(callback);
} }
}); });
} }
/** /**
* Update (or create) the change with the current revision. * Update (or create) the change with the current revision.
* *
* @callback {Function} callback * @callback {Function} callback
@ -156,7 +136,7 @@ Change.findOrCreateChange = function(modelName, modelId, callback) {
* @param {Change} change * @param {Change} change
*/ */
Change.prototype.rectify = function(cb) { Change.prototype.rectify = function(cb) {
var change = this; var change = this;
var tasks = [ var tasks = [
updateRevision, updateRevision,
@ -167,12 +147,12 @@ Change.prototype.rectify = function(cb) {
change.debug('rectify change'); change.debug('rectify change');
cb = cb || function(err) { cb = cb || function(err) {
if(err) throw new Error(err); if (err) throw new Error(err);
} }
async.parallel(tasks, function(err) { async.parallel(tasks, function(err) {
if(err) return cb(err); if (err) return cb(err);
if(change.prev === Change.UNKNOWN) { if (change.prev === Change.UNKNOWN) {
// this occurs when a record of a change doesn't exist // this occurs when a record of a change doesn't exist
// and its current revision is null (not found) // and its current revision is null (not found)
change.remove(cb); change.remove(cb);
@ -184,10 +164,10 @@ Change.prototype.rectify = function(cb) {
function updateRevision(cb) { function updateRevision(cb) {
// get the current revision // get the current revision
change.currentRevision(function(err, rev) { change.currentRevision(function(err, rev) {
if(err) return Change.handleError(err, cb); if (err) return Change.handleError(err, cb);
if(rev) { if (rev) {
// avoid setting rev and prev to the same value // avoid setting rev and prev to the same value
if(currentRev !== rev) { if (currentRev !== rev) {
change.rev = rev; change.rev = rev;
change.prev = currentRev; change.prev = currentRev;
} else { } else {
@ -195,9 +175,9 @@ Change.prototype.rectify = function(cb) {
} }
} else { } else {
change.rev = null; change.rev = null;
if(currentRev) { if (currentRev) {
change.prev = currentRev; change.prev = currentRev;
} else if(!change.prev) { } else if (!change.prev) {
change.debug('ERROR - could not determing prev'); change.debug('ERROR - could not determing prev');
change.prev = Change.UNKNOWN; change.prev = Change.UNKNOWN;
} }
@ -209,34 +189,34 @@ Change.prototype.rectify = function(cb) {
function updateCheckpoint(cb) { function updateCheckpoint(cb) {
change.constructor.getCheckpointModel().current(function(err, checkpoint) { change.constructor.getCheckpointModel().current(function(err, checkpoint) {
if(err) return Change.handleError(err); if (err) return Change.handleError(err);
change.checkpoint = checkpoint; change.checkpoint = checkpoint;
cb(); cb();
}); });
} }
} }
/** /**
* Get a change's current revision based on current data. * Get a change's current revision based on current data.
* @callback {Function} callback * @callback {Function} callback
* @param {Error} err * @param {Error} err
* @param {String} rev The current revision * @param {String} rev The current revision
*/ */
Change.prototype.currentRevision = function(cb) { Change.prototype.currentRevision = function(cb) {
var model = this.getModelCtor(); var model = this.getModelCtor();
var id = this.getModelId(); var id = this.getModelId();
model.findById(id, function(err, inst) { model.findById(id, function(err, inst) {
if(err) return Change.handleError(err, cb); if (err) return Change.handleError(err, cb);
if(inst) { if (inst) {
cb(null, Change.revisionForInst(inst)); cb(null, Change.revisionForInst(inst));
} else { } else {
cb(null, null); cb(null, null);
} }
}); });
} }
/** /**
* Create a hash of the given `string` with the `options.hashAlgorithm`. * Create a hash of the given `string` with the `options.hashAlgorithm`.
* **Default: `sha1`** * **Default: `sha1`**
* *
@ -244,24 +224,24 @@ Change.prototype.currentRevision = function(cb) {
* @return {String} The hashed string * @return {String} The hashed string
*/ */
Change.hash = function(str) { Change.hash = function(str) {
return crypto return crypto
.createHash(Change.settings.hashAlgorithm || 'sha1') .createHash(Change.settings.hashAlgorithm || 'sha1')
.update(str) .update(str)
.digest('hex'); .digest('hex');
} }
/** /**
* Get the revision string for the given object * Get the revision string for the given object
* @param {Object} inst The data to get the revision string for * @param {Object} inst The data to get the revision string for
* @return {String} The revision string * @return {String} The revision string
*/ */
Change.revisionForInst = function(inst) { Change.revisionForInst = function(inst) {
return this.hash(CJSON.stringify(inst)); return this.hash(CJSON.stringify(inst));
} }
/** /**
* Get a change's type. Returns one of: * Get a change's type. Returns one of:
* *
* - `Change.UPDATE` * - `Change.UPDATE`
@ -272,69 +252,69 @@ Change.revisionForInst = function(inst) {
* @return {String} the type of change * @return {String} the type of change
*/ */
Change.prototype.type = function() { Change.prototype.type = function() {
if(this.rev && this.prev) { if (this.rev && this.prev) {
return Change.UPDATE; return Change.UPDATE;
} }
if(this.rev && !this.prev) { if (this.rev && !this.prev) {
return Change.CREATE; return Change.CREATE;
} }
if(!this.rev && this.prev) { if (!this.rev && this.prev) {
return Change.DELETE; return Change.DELETE;
} }
return Change.UNKNOWN; return Change.UNKNOWN;
} }
/** /**
* Compare two changes. * Compare two changes.
* @param {Change} change * @param {Change} change
* @return {Boolean} * @return {Boolean}
*/ */
Change.prototype.equals = function(change) { Change.prototype.equals = function(change) {
if(!change) return false; if (!change) return false;
var thisRev = this.rev || null; var thisRev = this.rev || null;
var thatRev = change.rev || null; var thatRev = change.rev || null;
return thisRev === thatRev; return thisRev === thatRev;
} }
/** /**
* Does this change conflict with the given change. * Does this change conflict with the given change.
* @param {Change} change * @param {Change} change
* @return {Boolean} * @return {Boolean}
*/ */
Change.prototype.conflictsWith = function(change) { Change.prototype.conflictsWith = function(change) {
if(!change) return false; if (!change) return false;
if(this.equals(change)) return false; if (this.equals(change)) return false;
if(Change.bothDeleted(this, change)) return false; if (Change.bothDeleted(this, change)) return false;
if(this.isBasedOn(change)) return false; if (this.isBasedOn(change)) return false;
return true; return true;
} }
/** /**
* Are both changes deletes? * Are both changes deletes?
* @param {Change} a * @param {Change} a
* @param {Change} b * @param {Change} b
* @return {Boolean} * @return {Boolean}
*/ */
Change.bothDeleted = function(a, b) { Change.bothDeleted = function(a, b) {
return a.type() === Change.DELETE return a.type() === Change.DELETE
&& b.type() === Change.DELETE; && b.type() === Change.DELETE;
} }
/** /**
* Determine if the change is based on the given change. * Determine if the change is based on the given change.
* @param {Change} change * @param {Change} change
* @return {Boolean} * @return {Boolean}
*/ */
Change.prototype.isBasedOn = function(change) { Change.prototype.isBasedOn = function(change) {
return this.prev === change.rev; return this.prev === change.rev;
} }
/** /**
* Determine the differences for a given model since a given checkpoint. * Determine the differences for a given model since a given checkpoint.
* *
* The callback will contain an error or `result`. * The callback will contain an error or `result`.
@ -364,7 +344,7 @@ Change.prototype.isBasedOn = function(change) {
* @param {Object} result See above. * @param {Object} result See above.
*/ */
Change.diff = function(modelName, since, remoteChanges, callback) { Change.diff = function(modelName, since, remoteChanges, callback) {
var remoteChangeIndex = {}; var remoteChangeIndex = {};
var modelIds = []; var modelIds = [];
remoteChanges.forEach(function(ch) { remoteChanges.forEach(function(ch) {
@ -381,7 +361,7 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
checkpoint: {gte: since} checkpoint: {gte: since}
} }
}, function(err, localChanges) { }, function(err, localChanges) {
if(err) return callback(err); if (err) return callback(err);
var deltas = []; var deltas = [];
var conflicts = []; var conflicts = [];
var localModelIds = []; var localModelIds = [];
@ -390,8 +370,8 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
localChange = new Change(localChange); localChange = new Change(localChange);
localModelIds.push(localChange.modelId); localModelIds.push(localChange.modelId);
var remoteChange = remoteChangeIndex[localChange.modelId]; var remoteChange = remoteChangeIndex[localChange.modelId];
if(remoteChange && !localChange.equals(remoteChange)) { if (remoteChange && !localChange.equals(remoteChange)) {
if(remoteChange.conflictsWith(localChange)) { if (remoteChange.conflictsWith(localChange)) {
remoteChange.debug('remote conflict'); remoteChange.debug('remote conflict');
localChange.debug('local conflict'); localChange.debug('local conflict');
conflicts.push(localChange); conflicts.push(localChange);
@ -403,7 +383,7 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
}); });
modelIds.forEach(function(id) { modelIds.forEach(function(id) {
if(localModelIds.indexOf(id) === -1) { if (localModelIds.indexOf(id) === -1) {
deltas.push(remoteChangeIndex[id]); deltas.push(remoteChangeIndex[id]);
} }
}); });
@ -413,49 +393,49 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
conflicts: conflicts conflicts: conflicts
}); });
}); });
} }
/** /**
* Correct all change list entries. * Correct all change list entries.
* @param {Function} callback * @param {Function} callback
*/ */
Change.rectifyAll = function(cb) { Change.rectifyAll = function(cb) {
debug('rectify all'); debug('rectify all');
var Change = this; var Change = this;
// this should be optimized // this should be optimized
this.find(function(err, changes) { this.find(function(err, changes) {
if(err) return cb(err); if (err) return cb(err);
changes.forEach(function(change) { changes.forEach(function(change) {
change = new Change(change); change = new Change(change);
change.rectify(); change.rectify();
}); });
}); });
} }
/** /**
* Get the checkpoint model. * Get the checkpoint model.
* @return {Checkpoint} * @return {Checkpoint}
*/ */
Change.getCheckpointModel = function() { Change.getCheckpointModel = function() {
var checkpointModel = this.Checkpoint; var checkpointModel = this.Checkpoint;
if(checkpointModel) return checkpointModel; if (checkpointModel) return checkpointModel;
this.checkpoint = checkpointModel = loopback.Checkpoint.extend('checkpoint'); this.checkpoint = checkpointModel = loopback.Checkpoint.extend('checkpoint');
assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName assert(this.dataSource, 'Cannot getCheckpointModel(): ' + this.modelName
+ ' is not attached to a dataSource'); + ' is not attached to a dataSource');
checkpointModel.attachTo(this.dataSource); checkpointModel.attachTo(this.dataSource);
return checkpointModel; return checkpointModel;
} }
Change.handleError = function(err) { Change.handleError = function(err) {
if(!this.settings.ignoreErrors) { if (!this.settings.ignoreErrors) {
throw err; throw err;
} }
} }
Change.prototype.debug = function() { Change.prototype.debug = function() {
if(debug.enabled) { if (debug.enabled) {
var args = Array.prototype.slice.call(arguments); var args = Array.prototype.slice.call(arguments);
debug.apply(this, args); debug.apply(this, args);
debug('\tid', this.id); debug('\tid', this.id);
@ -465,33 +445,33 @@ Change.prototype.debug = function() {
debug('\tmodelId', this.modelId); debug('\tmodelId', this.modelId);
debug('\ttype', this.type()); debug('\ttype', this.type());
} }
} }
/** /**
* Get the `Model` class for `change.modelName`. * Get the `Model` class for `change.modelName`.
* @return {Model} * @return {Model}
*/ */
Change.prototype.getModelCtor = function() { Change.prototype.getModelCtor = function() {
return this.constructor.settings.trackModel; return this.constructor.settings.trackModel;
} }
Change.prototype.getModelId = function() { Change.prototype.getModelId = function() {
// TODO(ritch) get rid of the need to create an instance // TODO(ritch) get rid of the need to create an instance
var Model = this.getModelCtor(); var Model = this.getModelCtor();
var id = this.modelId; var id = this.modelId;
var m = new Model(); var m = new Model();
m.setId(id); m.setId(id);
return m.getId(); return m.getId();
} }
Change.prototype.getModel = function(callback) { Change.prototype.getModel = function(callback) {
var Model = this.constructor.settings.trackModel; var Model = this.constructor.settings.trackModel;
var id = this.getModelId(); var id = this.getModelId();
Model.findById(id, callback); Model.findById(id, callback);
} }
/** /**
* When two changes conflict a conflict is created. * When two changes conflict a conflict is created.
* *
* **Note: call `conflict.fetch()` to get the `target` and `source` models. * **Note: call `conflict.fetch()` to get the `target` and `source` models.
@ -501,17 +481,18 @@ Change.prototype.getModel = function(callback) {
* @param {PersistedModel} TargetModel * @param {PersistedModel} TargetModel
* @property {ModelClass} source The source model instance * @property {ModelClass} source The source model instance
* @property {ModelClass} target The target model instance * @property {ModelClass} target The target model instance
* @class Change.Conflict
*/ */
function Conflict(modelId, SourceModel, TargetModel) { function Conflict(modelId, SourceModel, TargetModel) {
this.SourceModel = SourceModel; this.SourceModel = SourceModel;
this.TargetModel = TargetModel; this.TargetModel = TargetModel;
this.SourceChange = SourceModel.getChangeModel(); this.SourceChange = SourceModel.getChangeModel();
this.TargetChange = TargetModel.getChangeModel(); this.TargetChange = TargetModel.getChangeModel();
this.modelId = modelId; this.modelId = modelId;
} }
/** /**
* Fetch the conflicting models. * Fetch the conflicting models.
* *
* @callback {Function} callback * @callback {Function} callback
@ -520,7 +501,7 @@ function Conflict(modelId, SourceModel, TargetModel) {
* @param {PersistedModel} target * @param {PersistedModel} target
*/ */
Conflict.prototype.models = function(cb) { Conflict.prototype.models = function(cb) {
var conflict = this; var conflict = this;
var SourceModel = this.SourceModel; var SourceModel = this.SourceModel;
var TargetModel = this.TargetModel; var TargetModel = this.TargetModel;
@ -534,7 +515,7 @@ Conflict.prototype.models = function(cb) {
function getSourceModel(cb) { function getSourceModel(cb) {
SourceModel.findById(conflict.modelId, function(err, model) { SourceModel.findById(conflict.modelId, function(err, model) {
if(err) return cb(err); if (err) return cb(err);
source = model; source = model;
cb(); cb();
}); });
@ -542,19 +523,19 @@ Conflict.prototype.models = function(cb) {
function getTargetModel(cb) { function getTargetModel(cb) {
TargetModel.findById(conflict.modelId, function(err, model) { TargetModel.findById(conflict.modelId, function(err, model) {
if(err) return cb(err); if (err) return cb(err);
target = model; target = model;
cb(); cb();
}); });
} }
function done(err) { function done(err) {
if(err) return cb(err); if (err) return cb(err);
cb(null, source, target); cb(null, source, target);
} }
} }
/** /**
* Get the conflicting changes. * Get the conflicting changes.
* *
* @callback {Function} callback * @callback {Function} callback
@ -563,7 +544,7 @@ Conflict.prototype.models = function(cb) {
* @param {Change} targetChange * @param {Change} targetChange
*/ */
Conflict.prototype.changes = function(cb) { Conflict.prototype.changes = function(cb) {
var conflict = this; var conflict = this;
var sourceChange; var sourceChange;
var targetChange; var targetChange;
@ -577,7 +558,7 @@ Conflict.prototype.changes = function(cb) {
conflict.SourceChange.findOne({where: { conflict.SourceChange.findOne({where: {
modelId: conflict.modelId modelId: conflict.modelId
}}, function(err, change) { }}, function(err, change) {
if(err) return cb(err); if (err) return cb(err);
sourceChange = change; sourceChange = change;
cb(); cb();
}); });
@ -587,35 +568,35 @@ Conflict.prototype.changes = function(cb) {
conflict.TargetChange.findOne({where: { conflict.TargetChange.findOne({where: {
modelId: conflict.modelId modelId: conflict.modelId
}}, function(err, change) { }}, function(err, change) {
if(err) return cb(err); if (err) return cb(err);
targetChange = change; targetChange = change;
cb(); cb();
}); });
} }
function done(err) { function done(err) {
if(err) return cb(err); if (err) return cb(err);
cb(null, sourceChange, targetChange); cb(null, sourceChange, targetChange);
} }
} }
/** /**
* Resolve the conflict. * Resolve the conflict.
* *
* @callback {Function} callback * @callback {Function} callback
* @param {Error} err * @param {Error} err
*/ */
Conflict.prototype.resolve = function(cb) { Conflict.prototype.resolve = function(cb) {
var conflict = this; var conflict = this;
conflict.changes(function(err, sourceChange, targetChange) { conflict.changes(function(err, sourceChange, targetChange) {
if(err) return cb(err); if (err) return cb(err);
sourceChange.prev = targetChange.rev; sourceChange.prev = targetChange.rev;
sourceChange.save(cb); sourceChange.save(cb);
}); });
} }
/** /**
* Determine the conflict type. * Determine the conflict type.
* *
* Possible results are * Possible results are
@ -629,18 +610,19 @@ Conflict.prototype.resolve = function(cb) {
* @param {String} type The conflict type. * @param {String} type The conflict type.
*/ */
Conflict.prototype.type = function(cb) { Conflict.prototype.type = function(cb) {
var conflict = this; var conflict = this;
this.changes(function(err, sourceChange, targetChange) { this.changes(function(err, sourceChange, targetChange) {
if(err) return cb(err); if (err) return cb(err);
var sourceChangeType = sourceChange.type(); var sourceChangeType = sourceChange.type();
var targetChangeType = targetChange.type(); var targetChangeType = targetChange.type();
if(sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) { if (sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) {
return cb(null, Change.UPDATE); return cb(null, Change.UPDATE);
} }
if(sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) { if (sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) {
return cb(null, Change.DELETE); return cb(null, Change.DELETE);
} }
return cb(null, Change.UNKNOWN); return cb(null, Change.UNKNOWN);
}); });
} }
};

25
common/models/change.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "Change",
"trackChanges": false,
"properties": {
"id": {
"type": "string",
"id": true
},
"rev": {
"type": "string"
},
"prev": {
"type": "string"
},
"checkpoint": {
"type": "number"
},
"modelName": {
"type": "string"
},
"modelId": {
"type": "string"
}
}
}

View File

@ -33,7 +33,9 @@ module.exports = function(loopback) {
require('../common/models/user.json'), require('../common/models/user.json'),
require('../common/models/user.js')); require('../common/models/user.js'));
loopback.Change = require('../common/models/change'); loopback.Change = createModel(
require('../common/models/change.json'),
require('../common/models/change.js'));
loopback.Checkpoint = createModel( loopback.Checkpoint = createModel(
require('../common/models/checkpoint.json'), require('../common/models/checkpoint.json'),

View File

@ -960,7 +960,10 @@ PersistedModel.enableChangeTracking = function() {
} }
PersistedModel._defineChangeModel = function() { PersistedModel._defineChangeModel = function() {
var BaseChangeModel = require('./../common/models/change'); var BaseChangeModel = loopback.Change;
assert(BaseChangeModel,
'Change model must be defined before enabling change replication');
return this.Change = BaseChangeModel.extend(this.modelName + '-change', return this.Change = BaseChangeModel.extend(this.modelName + '-change',
{}, {},
{ {