<commit_before>define([
	'compose',
	'lodash/objects/mapValues',
	'lodash/objects/clone',
	'ksf/utils/csv',
], function(
	compose,
	mapValues,
	clone,
	convert2csv
){
	var StoreMutationAPI = {
		add: function(value, key) {
			var changeArg = {};
			changeArg[key] = {add: value};
			var changes = this._change(changeArg);
			return Object.keys(changes)[0];
		},
		remove: function(key) {
			var changeArg = {};
			changeArg[key] = {remove: true};
			return this._change(changeArg)[key];
		},
		onChange: function(cb) {
			return this._onChange(cb);
		},
	};

	// méthodes de création d'accesseur
	var StoreAccessorsAPI = {
		item: function(key) {
			return this._item(key);
		},
		filter: function(filterFn) {
			return new FilterAccessor(this, filterFn);
		},
		sub: function(prop, value) {
			return new SubStoreAccessor(this, prop, value);
		},
		sort: function(sortFn) {
			return new SortedAccessor(this, sortFn);
		},
		count: function() {
			return new CountAccessor(this);
		},
		reduce: function(reduceFn, initialValue) {
			return new ReduceAccessor(this, reduceFn, initialValue);
		},
		partial: function(props) {
			return new PartialAccessor(this, props);
		},
		withComputedProperty: function(prop, deps, computeFn) {
			return new WithComputedPropertyAccessor(this, prop, deps, computeFn);
		},
		sum: function(prop) {
			return new SumAccessor(this, prop);
		},
		prop: function(prop) {
			return new (this._getPropAccessorFactory(prop))(this);
		},
		csv: function(columnsConfig) {
			var storeValue = this._getValue();
			return convert2csv(Object.keys(storeValue).map(function(key) {
				var itemValue = clone(storeValue[key]);
				itemValue.id = key;
				return itemValue;
			}), columnsConfig);
		},
		onChange: function(cb) {
			return this._onChange(cb);
		},
	};

	/**
		Un itemAccessor permet d'accéder à la valeur d'un élément du store par id.
		Potentiellement, on peut demander un itemAccessor pour un élément qui n'existe pas (son id n'est pas connu dans le store). Son _getValue renvoi alors 'undefined', c'est à dire qu'il ne répond pas (il pourrait même déclencher une erreur). Il pourrait avoir une méthode 'exists' qui permet de savoir s'il existe avant d'appeler '_getValue'.
		C'est pourquoi, sur 'onChange', on récupère uniquement les changements de valeurs et pas l'événement de destruction. Il faut s'abonner à 'onDelete' pour savoir que l'élément a été supprimé sur le store, que 'exists()' va renvoyer 'faux' et qu'il ne faut pas appeler '_getValue' et qu'on ne peut pas appeler '_change' non plus
	*/
	var ItemAccessor = compose(function(source, key) {
		this._source = source;
		this._key = key;
	}, {
		_exists: function() {
			return this._key in this._source._getValue();
		},
		_getValue: function() {
			return this._source._getValue()[this._key];
		},

		_change: function(changeArg) {
			var sourceChangeArg = {};
			sourceChangeArg[this._key] = { change: changeArg };
			return this._source._change(sourceChangeArg)[this._key].change;
		},
		_onChange: function(cb) {
			var key = this._key;
			return this._source._onChange(function(sourceChanges) {
				var itemChange = sourceChanges[key];
				if (itemChange && itemChange.change) {
					cb(itemChange.change);
				}
			});
		},
		'delete': function() {
			return this._source.remove(this._key);
		},
		// la méthode 'create' n'est pas très utile, mais c'est pour montrer le parallèle avec 'delete' qui en sont en fait que des redirections vers 'add' et 'remove' de la source.
		create: function() {
			return this._source.add(this._key);
		},
		onDelete: function(cb) {
			var key = this._key;
			return this._source._onChange(function(sourceChanges) {
				var itemChange = sourceChanges[key];
				if (itemChange && itemChange.remove) {
					cb();
				}
			});
		},
	});

	// TODO: à découper
	var AccessorBase = {
		_item: function(key) {
			return this._source._item(key);
		},
		onChange: function(cb) {
			return this._onChange(cb);
		},
		count: function() {
			return new CountAccessor(this);
		},
	};

	var FilterAccessor = compose(AccessorBase, StoreAccessorsAPI, function(source, filterFn) {
		this._source = source;
		this._filterFn = filterFn;
	},
	// as store source mixin
	{
		_getValue: function() {
			var self = this,
				value = this._source._getValue();
			var ret = {};
			Object.keys(value).forEach(function(key) {
				var item = value[key];
				if (self._filterFn(item, key)) {
					ret[key] = item;
				}
			});
			return ret;
		},
		_computeChanges: function(changes) {
			// TODO: filtrer les changes, ne rien retourner s'il n'y a pas de changes qui passent le filtre
			return changes;
		},
		_onChange: function(cb) {
			var self = this;
			return this._source._onChange(function(sourceChanges) {
				var changes = self._computeChanges(sourceChanges);
				if (changes) {
					cb(changes);
				}
			});
		},
		_getPropAccessorFactory: function(prop) {
			return this._source._getPropAccessorFactories(prop);
		},
		item: function(key) {
			return this._item(key);
		},
	});

	var SubStoreAccessor = compose(FilterAccessor, function(source, prop, value) {
		this._filterProp = prop;
		this._filterValue = value;
		this._filterFn = function(itemValue) {
			return itemValue[prop] === value;
		};
	}, {
		add: function(value, key) {
			value = value || {};
			value[this._filterProp] = this._filterValue;
			var changeArg = {};
			changeArg[key] = {add: value};
			var changes = this._source._change(changeArg);
			return Object.keys(changes)[0];
		},
		remove: function(key) {
			return this._source.remove(key);
		}
	});

	var SortedAccessor = compose(AccessorBase, function(source, compareFn) {
		this._source = source;
		this._compareFn = compareFn;
	}, {
		_getValue: function() {
			var source = this._source._getValue();
			var compareFn = this._compareFn;
			return Object.keys(source).sort(function(key1, key2) {
				return compareFn(source[key1], source[key2]);
			});<commit_msg>use deepComparison in subStore<commit_after>define([
	'compose',
	'lodash/objects/mapValues',
	'lodash/objects/clone',
	'lodash/objects/isEqual',
	'ksf/utils/csv',
], function(
	compose,
	mapValues,
	clone,
	isEqual,
	convert2csv
){
	var StoreMutationAPI = {
		add: function(value, key) {
			var changeArg = {};
			changeArg[key] = {add: value};
			var changes = this._change(changeArg);
			return Object.keys(changes)[0];
		},
		remove: function(key) {
			var changeArg = {};
			changeArg[key] = {remove: true};
			return this._change(changeArg)[key];
		},
		onChange: function(cb) {
			return this._onChange(cb);
		},
	};

	// méthodes de création d'accesseur
	var StoreAccessorsAPI = {
		item: function(key) {
			return this._item(key);
		},
		filter: function(filterFn) {
			return new FilterAccessor(this, filterFn);
		},
		sub: function(prop, value) {
			return new SubStoreAccessor(this, prop, value);
		},
		sort: function(sortFn) {
			return new SortedAccessor(this, sortFn);
		},
		count: function() {
			return new CountAccessor(this);
		},
		reduce: function(reduceFn, initialValue) {
			return new ReduceAccessor(this, reduceFn, initialValue);
		},
		partial: function(props) {
			return new PartialAccessor(this, props);
		},
		withComputedProperty: function(prop, deps, computeFn) {
			return new WithComputedPropertyAccessor(this, prop, deps, computeFn);
		},
		sum: function(prop) {
			return new SumAccessor(this, prop);
		},
		prop: function(prop) {
			return new (this._getPropAccessorFactory(prop))(this);
		},
		csv: function(columnsConfig) {
			var storeValue = this._getValue();
			return convert2csv(Object.keys(storeValue).map(function(key) {
				var itemValue = clone(storeValue[key]);
				itemValue.id = key;
				return itemValue;
			}), columnsConfig);
		},
		onChange: function(cb) {
			return this._onChange(cb);
		},
	};

	/**
		Un itemAccessor permet d'accéder à la valeur d'un élément du store par id.
		Potentiellement, on peut demander un itemAccessor pour un élément qui n'existe pas (son id n'est pas connu dans le store). Son _getValue renvoi alors 'undefined', c'est à dire qu'il ne répond pas (il pourrait même déclencher une erreur). Il pourrait avoir une méthode 'exists' qui permet de savoir s'il existe avant d'appeler '_getValue'.
		C'est pourquoi, sur 'onChange', on récupère uniquement les changements de valeurs et pas l'événement de destruction. Il faut s'abonner à 'onDelete' pour savoir que l'élément a été supprimé sur le store, que 'exists()' va renvoyer 'faux' et qu'il ne faut pas appeler '_getValue' et qu'on ne peut pas appeler '_change' non plus
	*/
	var ItemAccessor = compose(function(source, key) {
		this._source = source;
		this._key = key;
	}, {
		_exists: function() {
			return this._key in this._source._getValue();
		},
		_getValue: function() {
			return this._source._getValue()[this._key];
		},

		_change: function(changeArg) {
			var sourceChangeArg = {};
			sourceChangeArg[this._key] = { change: changeArg };
			return this._source._change(sourceChangeArg)[this._key].change;
		},
		_onChange: function(cb) {
			var key = this._key;
			return this._source._onChange(function(sourceChanges) {
				var itemChange = sourceChanges[key];
				if (itemChange && itemChange.change) {
					cb(itemChange.change);
				}
			});
		},
		'delete': function() {
			return this._source.remove(this._key);
		},
		// la méthode 'create' n'est pas très utile, mais c'est pour montrer le parallèle avec 'delete' qui en sont en fait que des redirections vers 'add' et 'remove' de la source.
		create: function() {
			return this._source.add(this._key);
		},
		onDelete: function(cb) {
			var key = this._key;
			return this._source._onChange(function(sourceChanges) {
				var itemChange = sourceChanges[key];
				if (itemChange && itemChange.remove) {
					cb();
				}
			});
		},
	});

	// TODO: à découper
	var AccessorBase = {
		_item: function(key) {
			return this._source._item(key);
		},
		onChange: function(cb) {
			return this._onChange(cb);
		},
		count: function() {
			return new CountAccessor(this);
		},
	};

	var FilterAccessor = compose(AccessorBase, StoreAccessorsAPI, function(source, filterFn) {
		this._source = source;
		this._filterFn = filterFn;
	},
	// as store source mixin
	{
		_getValue: function() {
			var self = this,
				value = this._source._getValue();
			var ret = {};
			Object.keys(value).forEach(function(key) {
				var item = value[key];
				if (self._filterFn(item, key)) {
					ret[key] = item;
				}
			});
			return ret;
		},
		_computeChanges: function(changes) {
			// TODO: filtrer les changes, ne rien retourner s'il n'y a pas de changes qui passent le filtre
			return changes;
		},
		_onChange: function(cb) {
			var self = this;
			return this._source._onChange(function(sourceChanges) {
				var changes = self._computeChanges(sourceChanges);
				if (changes) {
					cb(changes);
				}
			});
		},
		_getPropAccessorFactory: function(prop) {
			return this._source._getPropAccessorFactories(prop);
		},
		item: function(key) {
			return this._item(key);
		},
	});

	var SubStoreAccessor = compose(FilterAccessor, function(source, prop, value) {
		this._filterProp = prop;
		this._filterValue = value;
		if (typeof value === 'object') {
			// deep compare
			this._filterFn = function(itemValue) {
				return isEqual(itemValue[prop], value);
			};
		} else {
			this._filterFn = function(itemValue) {
				return itemValue[prop] === value;
			};
		}
	}, {
		add: function(value, key) {
			value = value || {};
			value[this._filterProp] = this._filterValue;
			var changeArg = {};
			changeArg[key] = {add: value};
			var changes = this._source._change(changeArg);
			return Object.keys(changes)[0];
		},
		remove: function(key) {
			return this._source.remove(key);
		}
	});

	var SortedAccessor = compose(AccessorBase, function(source, compareFn) {
		this._source = source;
		this._compareFn = compareFn;
	}, {
		_getValue: function() {
			var source = this._source._getValue();
			var compareFn = this._compareFn;
			return Object.keys(source).sort(function(key1, key2) {
				return compareFn(source[key1], source[key2]);
			});
{'fraction_non_alphanumeric': 0.13394190871369294, 'fraction_numerical': 0.0013278008298755186, 'mean_word_length': 3.7935560859188544, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '396576', 'n_tokens_mistral': 4527, 'n_tokens_neox': 3946, 'n_words': 999}