mirror of
https://github.com/discourse/discourse.git
synced 2025-02-04 08:05:15 +08:00
296 lines
8.2 KiB
JavaScript
296 lines
8.2 KiB
JavaScript
import RestModel from 'discourse/models/rest';
|
|
import ResultSet from 'discourse/models/result-set';
|
|
|
|
let _identityMap;
|
|
|
|
// You should only call this if you're a test scaffold
|
|
function flushMap() {
|
|
_identityMap = {};
|
|
}
|
|
|
|
function storeMap(type, id, obj) {
|
|
if (!id) { return; }
|
|
|
|
_identityMap[type] = _identityMap[type] || {};
|
|
_identityMap[type][id] = obj;
|
|
}
|
|
|
|
function fromMap(type, id) {
|
|
const byType = _identityMap[type];
|
|
if (byType) { return byType[id]; }
|
|
}
|
|
|
|
function removeMap(type, id) {
|
|
const byType = _identityMap[type];
|
|
if (byType) { delete byType[id]; }
|
|
}
|
|
|
|
function findAndRemoveMap(type, id) {
|
|
const byType = _identityMap[type];
|
|
if (byType) {
|
|
const result = byType[id];
|
|
delete byType[id];
|
|
return result;
|
|
}
|
|
}
|
|
|
|
flushMap();
|
|
|
|
export default Ember.Object.extend({
|
|
_plurals: {'post-reply': 'post-replies',
|
|
'post-reply-history': 'post_reply_histories'},
|
|
|
|
pluralize(thing) {
|
|
return this._plurals[thing] || thing + "s";
|
|
},
|
|
|
|
addPluralization(thing, plural) {
|
|
this._plurals[thing] = plural;
|
|
},
|
|
|
|
findAll(type) {
|
|
const self = this;
|
|
return this.adapterFor(type).findAll(this, type).then(function(result) {
|
|
return self._resultSet(type, result);
|
|
});
|
|
},
|
|
|
|
// Mostly for legacy, things like TopicList without ResultSets
|
|
findFiltered(type, findArgs) {
|
|
const self = this;
|
|
return this.adapterFor(type).find(this, type, findArgs).then(function(result) {
|
|
return self._build(type, result);
|
|
});
|
|
},
|
|
|
|
_hydrateFindResults(result, type, findArgs) {
|
|
if (typeof findArgs === "object") {
|
|
return this._resultSet(type, result, findArgs);
|
|
} else {
|
|
return this._hydrate(type, result[Ember.String.underscore(type)], result);
|
|
}
|
|
},
|
|
|
|
// See if the store can find stale data. We sometimes prefer to show stale data and
|
|
// refresh it in the background.
|
|
findStale(type, findArgs, opts) {
|
|
const stale = this.adapterFor(type).findStale(this, type, findArgs, opts);
|
|
return {
|
|
hasResults: (stale !== undefined),
|
|
results: stale,
|
|
refresh: () => this.find(type, findArgs, opts)
|
|
};
|
|
},
|
|
|
|
find(type, findArgs, opts) {
|
|
var adapter = this.adapterFor(type);
|
|
return adapter.find(this, type, findArgs, opts).then(result => {
|
|
var hydrated = this._hydrateFindResults(result, type, findArgs, opts);
|
|
if (adapter.cache) {
|
|
const stale = adapter.findStale(this, type, findArgs, opts);
|
|
hydrated = this._updateStale(stale, hydrated);
|
|
adapter.cacheFind(this, type, findArgs, opts, hydrated);
|
|
}
|
|
return hydrated;
|
|
});
|
|
},
|
|
|
|
_updateStale(stale, hydrated) {
|
|
if (!stale) {
|
|
return hydrated;
|
|
}
|
|
|
|
hydrated.set('content', hydrated.get('content').map((item) => {
|
|
var staleItem = stale.content.findBy('id', item.get('id'));
|
|
if (staleItem) {
|
|
staleItem.setProperties(item);
|
|
} else {
|
|
staleItem = item;
|
|
}
|
|
return staleItem;
|
|
}));
|
|
return hydrated;
|
|
},
|
|
|
|
refreshResults(resultSet, type, url) {
|
|
const self = this;
|
|
return Discourse.ajax(url).then(result => {
|
|
const typeName = Ember.String.underscore(self.pluralize(type));
|
|
const content = result[typeName].map(obj => self._hydrate(type, obj, result));
|
|
resultSet.set('content', content);
|
|
});
|
|
},
|
|
|
|
appendResults(resultSet, type, url) {
|
|
const self = this;
|
|
|
|
return Discourse.ajax(url).then(function(result) {
|
|
const typeName = Ember.String.underscore(self.pluralize(type)),
|
|
totalRows = result["total_rows_" + typeName] || result.get('totalRows'),
|
|
loadMoreUrl = result["load_more_" + typeName],
|
|
content = result[typeName].map(obj => self._hydrate(type, obj, result));
|
|
|
|
resultSet.setProperties({ totalRows, loadMoreUrl });
|
|
resultSet.get('content').pushObjects(content);
|
|
|
|
// If we've loaded them all, clear the load more URL
|
|
if (resultSet.get('length') >= totalRows) {
|
|
resultSet.set('loadMoreUrl', null);
|
|
}
|
|
});
|
|
},
|
|
|
|
update(type, id, attrs) {
|
|
return this.adapterFor(type).update(this, type, id, attrs, function(result) {
|
|
if (result && result[type] && result[type].id) {
|
|
const oldRecord = findAndRemoveMap(type, id);
|
|
storeMap(type, result[type].id, oldRecord);
|
|
}
|
|
return result;
|
|
});
|
|
},
|
|
|
|
createRecord(type, attrs) {
|
|
attrs = attrs || {};
|
|
return !!attrs.id ? this._hydrate(type, attrs) : this._build(type, attrs);
|
|
},
|
|
|
|
destroyRecord(type, record) {
|
|
// If the record is new, don't perform an Ajax call
|
|
if (record.get('isNew')) {
|
|
removeMap(type, record.get('id'));
|
|
return Ember.RSVP.Promise.resolve(true);
|
|
}
|
|
|
|
return this.adapterFor(type).destroyRecord(this, type, record).then(function(result) {
|
|
removeMap(type, record.get('id'));
|
|
return result;
|
|
});
|
|
},
|
|
|
|
_resultSet(type, result, findArgs) {
|
|
const typeName = Ember.String.underscore(this.pluralize(type));
|
|
const content = result[typeName].map(obj => this._hydrate(type, obj, result));
|
|
|
|
const createArgs = {
|
|
content,
|
|
findArgs,
|
|
totalRows: result["total_rows_" + typeName] || content.length,
|
|
loadMoreUrl: result["load_more_" + typeName],
|
|
refreshUrl: result['refresh_' + typeName],
|
|
store: this,
|
|
__type: type
|
|
};
|
|
|
|
if (result.extras) {
|
|
createArgs.extras = result.extras;
|
|
}
|
|
|
|
return ResultSet.create(createArgs);
|
|
},
|
|
|
|
_build(type, obj) {
|
|
obj.store = this;
|
|
obj.__type = type;
|
|
obj.__state = obj.id ? "created" : "new";
|
|
|
|
// TODO: Have injections be automatic
|
|
obj.topicTrackingState = this.container.lookup('topic-tracking-state:main');
|
|
obj.keyValueStore = this.container.lookup('key-value-store:main');
|
|
|
|
const klass = this.container.lookupFactory('model:' + type) || RestModel;
|
|
const model = klass.create(obj);
|
|
|
|
storeMap(type, obj.id, model);
|
|
return model;
|
|
},
|
|
|
|
adapterFor(type) {
|
|
return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
|
|
},
|
|
|
|
_lookupSubType(subType, type, id, root) {
|
|
|
|
// cheat: we know we already have categories in memory
|
|
// TODO: topics do their own resolving of `category_id`
|
|
// to category. That should either respect this or be
|
|
// removed.
|
|
if (subType === 'category' && type !== 'topic') {
|
|
return Discourse.Category.findById(id);
|
|
}
|
|
|
|
const pluralType = this.pluralize(subType);
|
|
const collection = root[this.pluralize(subType)];
|
|
if (collection) {
|
|
const hashedProp = "__hashed_" + pluralType;
|
|
let hashedCollection = root[hashedProp];
|
|
if (!hashedCollection) {
|
|
hashedCollection = {};
|
|
collection.forEach(function(it) {
|
|
hashedCollection[it.id] = it;
|
|
});
|
|
root[hashedProp] = hashedCollection;
|
|
}
|
|
|
|
const found = hashedCollection[id];
|
|
if (found) {
|
|
const hydrated = this._hydrate(subType, found, root);
|
|
hashedCollection[id] = hydrated;
|
|
return hydrated;
|
|
}
|
|
}
|
|
},
|
|
|
|
_hydrateEmbedded(type, obj, root) {
|
|
const self = this;
|
|
Object.keys(obj).forEach(function(k) {
|
|
const m = /(.+)\_id(s?)$/.exec(k);
|
|
if (m) {
|
|
const subType = m[1];
|
|
|
|
if (m[2]) {
|
|
const hydrated = obj[k].map(function(id) {
|
|
return self._lookupSubType(subType, type, id, root);
|
|
});
|
|
obj[self.pluralize(subType)] = hydrated || [];
|
|
delete obj[k];
|
|
} else {
|
|
const hydrated = self._lookupSubType(subType, type, obj[k], root);
|
|
if (hydrated) {
|
|
obj[subType] = hydrated;
|
|
delete obj[k];
|
|
}
|
|
}
|
|
|
|
}
|
|
});
|
|
},
|
|
|
|
_hydrate(type, obj, root) {
|
|
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
|
|
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }
|
|
|
|
root = root || obj;
|
|
|
|
// Experimental: If serialized with a certain option we'll wire up embedded objects
|
|
// automatically.
|
|
if (root.__rest_serializer === "1") {
|
|
this._hydrateEmbedded(type, obj, root);
|
|
}
|
|
|
|
const existing = fromMap(type, obj.id);
|
|
if (existing === obj) { return existing; }
|
|
|
|
if (existing) {
|
|
delete obj.id;
|
|
const klass = this.container.lookupFactory('model:' + type) || RestModel;
|
|
existing.setProperties(klass.munge(obj));
|
|
return existing;
|
|
}
|
|
|
|
return this._build(type, obj);
|
|
}
|
|
});
|
|
|
|
export { flushMap };
|