Sync mongo methods for the client again

This commit is contained in:
denihs
2023-02-02 16:48:29 -04:00
parent 937800007c
commit 3e5977ce2b
7 changed files with 565 additions and 104 deletions

View File

@@ -87,6 +87,9 @@ CollectionPrototype._defineMutationMethods = function(options) {
self._insecure = undefined;
self._validators = {
insert: {allow: [], deny: []},
update: {allow: [], deny: []},
remove: {allow: [], deny: []},
insertAsync: {allow: [], deny: []},
updateAsync: {allow: [], deny: []},
removeAsync: {allow: [], deny: []},
@@ -110,26 +113,40 @@ CollectionPrototype._defineMutationMethods = function(options) {
if (self._connection && (self._connection === Meteor.server || Meteor.isClient)) {
const m = {};
['insertAsync', 'updateAsync', 'removeAsync'].forEach((method) => {
[
'insertAsync',
'updateAsync',
'removeAsync',
'insert',
'update',
'remove',
].forEach(method => {
const methodName = self._prefix + method;
if (options.useExisting) {
const handlerPropName = Meteor.isClient ? '_methodHandlers' : 'method_handlers';
const handlerPropName = Meteor.isClient
? '_methodHandlers'
: 'method_handlers';
// Do not try to create additional methods if this has already been called.
// (Otherwise the .methods() call below will throw an error.)
if (self._connection[handlerPropName] &&
typeof self._connection[handlerPropName][methodName] === 'function') return;
if (
self._connection[handlerPropName] &&
typeof self._connection[handlerPropName][methodName] === 'function'
)
return;
}
const isInsert = name => name.includes('insert');
m[methodName] = function (/* ... */) {
// All the methods do their own validation, instead of using check().
check(arguments, [Match.Any]);
const args = Array.from(arguments);
try {
// For an insertAsync, if the client didn't specify an _id, generate one
// For an insert/insertAsync, if the client didn't specify an _id, generate one
// now; because this uses DDP.randomStream, it will be consistent with
// what the client generated. We generate it now rather than later so
// that if (eg) an allow/deny rule does an insertAsync to the same
// that if (eg) an allow/deny rule does an insert/insertAsync to the same
// collection (not that it really should), the generated _id will
// still be the first use of the stream and will be consistent.
//
@@ -138,42 +155,44 @@ CollectionPrototype._defineMutationMethods = function(options) {
// between arbitrary client-specified _id fields and merely
// client-controlled-via-randomSeed fields.
let generatedId = null;
if (method === "insertAsync" && !hasOwn.call(args[0], '_id')) {
if (isInsert(method) && !hasOwn.call(args[0], '_id')) {
generatedId = self._makeNewID();
}
if (this.isSimulation) {
// In a client simulation, you can do any mutation (even with a
// complex selector).
if (generatedId !== null)
if (generatedId !== null) {
args[0]._id = generatedId;
return self._collection[method].apply(
self._collection, args);
}
return self._collection[method].apply(self._collection, args);
}
// This is the server receiving a method call from the client.
// We don't allow arbitrary selectors in mutations from the client: only
// single-ID selectors.
if (method !== 'insertAsync')
throwIfSelectorIsNotId(args[0], method);
if (!isInsert(method)) throwIfSelectorIsNotId(args[0], method);
if (self._restricted) {
// short circuit if there is no way it will pass.
if (self._validators[method].allow.length === 0) {
throw new Meteor.Error(
403, "Access denied. No allow validators set on restricted " +
"collection for method '" + method + "'.");
403,
'Access denied. No allow validators set on restricted ' +
"collection for method '" +
method +
"'."
);
}
const validatedMethodName =
'_validated' + method.charAt(0).toUpperCase() + method.slice(1);
args.unshift(this.userId);
method === 'insertAsync' && args.push(generatedId);
isInsert(method) && args.push(generatedId);
return self[validatedMethodName].apply(self, args);
} else if (self._isInsecure()) {
if (generatedId !== null)
args[0]._id = generatedId;
if (generatedId !== null) args[0]._id = generatedId;
// In insecure mode, allow any mutation (with a simple selector).
// XXX This is kind of bogus. Instead of blindly passing whatever
// we get from the network to this function, we should actually
@@ -189,7 +208,7 @@ CollectionPrototype._defineMutationMethods = function(options) {
} else {
// In secure mode, if we haven't called allow or deny, then nothing
// is permitted.
throw new Meteor.Error(403, "Access denied");
throw new Meteor.Error(403, 'Access denied');
}
} catch (e) {
if (
@@ -261,7 +280,37 @@ CollectionPrototype._validatedInsertAsync = function (userId, doc,
if (generatedId !== null)
doc._id = generatedId;
self._collection.insertAsync.call(self._collection, doc);
return self._collection.insertAsync.call(self._collection, doc);
};
CollectionPrototype._validatedInsert = function (userId, doc,
generatedId) {
const self = this;
// call user validators.
// Any deny returns true means denied.
if (self._validators.insert.deny.some((validator) => {
return validator(userId, docToValidate(validator, doc, generatedId));
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (self._validators.insert.allow.every((validator) => {
return !validator(userId, docToValidate(validator, doc, generatedId));
})) {
throw new Meteor.Error(403, "Access denied");
}
// If we generated an ID above, insert it now: after the validation, but
// before actually inserting.
if (generatedId !== null)
doc._id = generatedId;
return (Meteor.isServer
? self._collection.insertAsync
: self._collection.insert
).call(self._collection, doc);
};
// Simulate a mongo `update` operation while validating that the access
@@ -364,6 +413,102 @@ CollectionPrototype._validatedUpdateAsync = async function(
self._collection, selector, mutator, options);
};
CollectionPrototype._validatedUpdate = function(
userId, selector, mutator, options) {
const self = this;
check(mutator, Object);
options = Object.assign(Object.create(null), options);
if (!LocalCollection._selectorIsIdPerhapsAsObject(selector))
throw new Error("validated update should be of a single ID");
// We don't support upserts because they don't fit nicely into allow/deny
// rules.
if (options.upsert)
throw new Meteor.Error(403, "Access denied. Upserts not " +
"allowed in a restricted collection.");
const noReplaceError = "Access denied. In a restricted collection you can only" +
" update documents, not replace them. Use a Mongo update operator, such " +
"as '$set'.";
const mutatorKeys = Object.keys(mutator);
// compute modified fields
const modifiedFields = {};
if (mutatorKeys.length === 0) {
throw new Meteor.Error(403, noReplaceError);
}
mutatorKeys.forEach((op) => {
const params = mutator[op];
if (op.charAt(0) !== '$') {
throw new Meteor.Error(403, noReplaceError);
} else if (!hasOwn.call(ALLOWED_UPDATE_OPERATIONS, op)) {
throw new Meteor.Error(
403, "Access denied. Operator " + op + " not allowed in a restricted collection.");
} else {
Object.keys(params).forEach((field) => {
// treat dotted fields as if they are replacing their
// top-level part
if (field.indexOf('.') !== -1)
field = field.substring(0, field.indexOf('.'));
// record the field we are trying to change
modifiedFields[field] = true;
});
}
});
const fields = Object.keys(modifiedFields);
const findOptions = {transform: null};
if (!self._validators.fetchAllFields) {
findOptions.fields = {};
self._validators.fetch.forEach((fieldName) => {
findOptions.fields[fieldName] = 1;
});
}
const doc = self._collection.findOne(selector, findOptions);
if (!doc) // none satisfied!
return 0;
// call user validators.
// Any deny returns true means denied.
if (self._validators.update.deny.some((validator) => {
const factoriedDoc = transformDoc(validator, doc);
return validator(userId,
factoriedDoc,
fields,
mutator);
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (self._validators.update.allow.every((validator) => {
const factoriedDoc = transformDoc(validator, doc);
return !validator(userId,
factoriedDoc,
fields,
mutator);
})) {
throw new Meteor.Error(403, "Access denied");
}
options._forbidReplace = true;
// Back when we supported arbitrary client-provided selectors, we actually
// rewrote the selector to include an _id clause before passing to Mongo to
// avoid races, but since selector is guaranteed to already just be an ID, we
// don't have to any more.
return self._collection.update.call(
self._collection, selector, mutator, options);
};
// Only allow these operations in validated updates. Specifically
// whitelist operations, rather than blacklist, so new complex
// operations that are added aren't automatically allowed. A complex
@@ -414,6 +559,43 @@ CollectionPrototype._validatedRemoveAsync = async function(userId, selector) {
return self._collection.removeAsync.call(self._collection, selector);
};
CollectionPrototype._validatedRemove = function(userId, selector) {
const self = this;
const findOptions = {transform: null};
if (!self._validators.fetchAllFields) {
findOptions.fields = {};
self._validators.fetch.forEach((fieldName) => {
findOptions.fields[fieldName] = 1;
});
}
const doc = self._collection.findOne(selector, findOptions);
if (!doc)
return 0;
// call user validators.
// Any deny returns true means denied.
if (self._validators.remove.deny.some((validator) => {
return validator(userId, transformDoc(validator, doc));
})) {
throw new Meteor.Error(403, "Access denied");
}
// Any allow returns true means proceed. Throw error if they all fail.
if (self._validators.remove.allow.every((validator) => {
return !validator(userId, transformDoc(validator, doc));
})) {
throw new Meteor.Error(403, "Access denied");
}
// Back when we supported arbitrary client-provided selectors, we actually
// rewrote the selector to {_id: {$in: [ids that we found]}} before passing to
// Mongo to avoid races, but since selector is guaranteed to already just be
// an ID, we don't have to any more.
return self._collection.remove.call(self._collection, selector);
};
CollectionPrototype._callMutatorMethodAsync = async function _callMutatorMethod(name, args) {
// For two out of three mutator methods, the first argument is a selector
@@ -429,6 +611,36 @@ CollectionPrototype._callMutatorMethodAsync = async function _callMutatorMethod(
return await this._connection.applyAsync(mutatorMethodName, args, { returnStubValue: true , throwStubExceptions: true });
}
CollectionPrototype._callMutatorMethod = function _callMutatorMethod(name, args, callback) {
if (Meteor.isClient && !callback && !alreadyInSimulation()) {
// Client can't block, so it can't report errors by exception,
// only by callback. If they forget the callback, give them a
// default one that logs the error, so they aren't totally
// baffled if their writes don't work because their database is
// down.
// Don't give a default callback in simulation, because inside stubs we
// want to return the results from the local collection immediately and
// not force a callback.
callback = function (err) {
if (err)
Meteor._debug(name + " failed", err);
};
}
// For two out of three mutator methods, the first argument is a selector
const firstArgIsSelector = name === "update" || name === "remove";
if (firstArgIsSelector && !alreadyInSimulation()) {
// If we're about to actually send an RPC, we should throw an error if
// this is a non-ID selector, because the mutation methods only allow
// single-ID selectors. (If we don't throw here, we'll see flicker.)
throwIfSelectorIsNotId(args[0], name);
}
const mutatorMethodName = this._prefix + name;
return this._connection.apply(
mutatorMethodName, args, { returnStubValue: true }, callback);
}
function transformDoc(validator, doc) {
if (validator.transform)
return validator.transform(doc);
@@ -454,7 +666,7 @@ function docToValidate(validator, doc, generatedId) {
function addValidator(collection, allowOrDeny, options) {
// validate keys
const validKeysRegEx = /^(?:insertAsync|updateAsync|removeAsync|fetch|transform)$/;
const validKeysRegEx = /^(?:insertAsync|updateAsync|removeAsync|insert|update|remove|fetch|transform)$/;
Object.keys(options).forEach((key) => {
if (!validKeysRegEx.test(key))
throw new Error(allowOrDeny + ": Invalid key: " + key);
@@ -462,20 +674,30 @@ function addValidator(collection, allowOrDeny, options) {
collection._restricted = true;
['insertAsync', 'updateAsync', 'removeAsync'].forEach((name) => {
[
'insertAsync',
'updateAsync',
'removeAsync',
'insert',
'update',
'remove',
].forEach(name => {
if (hasOwn.call(options, name)) {
if (!(options[name] instanceof Function)) {
throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function");
throw new Error(
allowOrDeny + ': Value for `' + name + '` must be a function'
);
}
// If the transform is specified at all (including as 'null') in this
// call, then take that; otherwise, take the transform from the
// collection.
if (options.transform === undefined) {
options[name].transform = collection._transform; // already wrapped
options[name].transform = collection._transform; // already wrapped
} else {
options[name].transform = LocalCollection.wrapTransform(
options.transform);
options.transform
);
}
collection._validators[name][allowOrDeny].push(options[name]);
}

View File

@@ -97,6 +97,15 @@ Ledger.allow({
removeAsync: function() {
return true;
},
insert: function() {
return true;
},
update: function() {
return true;
},
remove: function() {
return true;
},
fetch: []
});

View File

@@ -381,46 +381,44 @@ const subscribeBeforeRun = async (subName, testId, cb) => {
// this is a big hack (and XXX pollutes the global test namespace)
testAsyncMulti('livedata - compound methods', [
async function(test) {
await Ledger.insertAsync({
name: 'alice',
balance: 100,
world: test.runId(),
});
await Ledger.insertAsync({ name: 'bob', balance: 50, world: test.runId() });
if (Meteor.isClient) {
Meteor.subscribe('ledger', test.runId(), () => {});
}
await Ledger.insertAsync(
{ name: 'alice', balance: 100, world: test.runId() }
);
await Ledger.insertAsync(
{ name: 'bob', balance: 50, world: test.runId() }
);
},
async function(test) {
await subscribeBeforeRun('ledger', test.runId(), async () => {
await Meteor.callAsync(
'ledger/transfer',
test.runId(),
'alice',
'bob',
10,
)
await checkBalances(test, 90, 60);
},
async function(test) {
try {
await Meteor.callAsync(
'ledger/transfer',
test.runId(),
'alice',
'bob',
10
100,
true,
);
await checkBalances(test, 90, 60);
});
},
async function(test) {
await subscribeBeforeRun('ledger', test.runId(), async () => {
try {
await Meteor.callAsync(
'ledger/transfer',
test.runId(),
'alice',
'bob',
100,
true
);
} catch (e) {}
if (Meteor.isClient) {
// client can fool itself by cheating, but only until the sync
// finishes
await checkBalances(test, -10, 160);
} else {
await checkBalances(test, 90, 60);
}
});
} catch (e) {
}
if (Meteor.isClient)
// client can fool itself by cheating, but only until the sync
// finishes
await checkBalances(test, -10, 160);
else await checkBalances(test, 90, 60);
},
]);

View File

@@ -328,9 +328,7 @@ var Session = function (server, version, socket, options) {
self.send({ msg: 'connected', session: self.id });
// On initial connect, spin up all the universal publishers.
Meteor._runAsync(function() {
self.startUniversalSubs();
});
self.startUniversalSubs();
if (version !== 'pre1' && options.heartbeatInterval !== 0) {
// We no longer need the low level timeout because we have heartbeats.
@@ -372,8 +370,15 @@ Object.assign(Session.prototype, {
sendAdded(collectionName, id, fields) {
if (this._canSend(collectionName))
this.send({msg: "added", collection: collectionName, id, fields});
if (this._canSend(collectionName)) {
if (!this._publishCursorPromise) {
this.send({ msg: 'added', collection: collectionName, id, fields });
return;
}
this._publishCursorPromise.finally(() =>
this.send({ msg: 'added', collection: collectionName, id, fields })
);
}
},
sendChanged(collectionName, id, fields) {
@@ -602,7 +607,7 @@ Object.assign(Session.prototype, {
unblock(); // in case the handler didn't already do it
}
Meteor._runAsync(runHandlers);
runHandlers();
};
processNext();
@@ -714,10 +719,13 @@ Object.assign(Session.prototype, {
// Find the handler
var handler = self.server.method_handlers[msg.method];
if (!handler) {
self.send({
msg: 'result', id: msg.id,
error: new Meteor.Error(404, `Method '${msg.method}' not found`)});
fence.arm();
fence.arm().finally(() => {
self.send({
msg: 'result',
id: msg.id,
error: new Meteor.Error(404, `Method '${msg.method}' not found`),
});
});
return;
}
@@ -790,8 +798,9 @@ Object.assign(Session.prototype, {
});
function finish() {
fence.arm();
unblock();
return fence.arm().finally(() => {
unblock();
});
}
const payload = {
@@ -800,18 +809,20 @@ Object.assign(Session.prototype, {
};
promise.then(result => {
finish();
if (result !== undefined) {
payload.result = result;
}
self.send(payload);
finish().then(() => {
if (result !== undefined) {
payload.result = result;
}
self.send(payload);
});
}, (exception) => {
finish();
payload.error = wrapInternalException(
exception,
`while invoking method '${msg.method}'`
);
self.send(payload);
finish().then(() => {
payload.error = wrapInternalException(
exception,
`while invoking method '${msg.method}'`
);
self.send(payload);
});
});
}
},
@@ -1190,7 +1201,7 @@ Object.assign(Subscription.prototype, {
// mark subscription as ready.
self.ready();
} else {
res._publishCursor(self).then(() => {
this._publishCursorPromise = res._publishCursor(self).then(() => {
self.ready();
}).catch((e) => self.error(e));
}
@@ -1226,7 +1237,7 @@ Object.assign(Subscription.prototype, {
}
self.ready();
} else {
Promise.all(res.map((c) => c._publishCursor(self))).then(() => {
this._publishCursorPromise = Promise.all(res.map((c) => c._publishCursor(self))).then(() => {
self.ready();
}).catch((e) => self.error(e));
}
@@ -1367,6 +1378,7 @@ Object.assign(Subscription.prototype, {
ids.add(id);
}
this._session._publishCursorPromise = this._publishCursorPromise;
this._session.added(this._subscriptionHandle, collectionName, id, fields);
},
@@ -1707,9 +1719,7 @@ Object.assign(Server.prototype, {
// self.sessions to change while we're running this loop.
self.sessions.forEach(function (session) {
if (!session._dontStartNewUniversalSubs) {
Meteor._runAsync(function() {
session._startSubscription(handler);
});
session._startSubscription(handler);
}
});
}

View File

@@ -155,7 +155,6 @@ export default class LocalCollection {
}
});
// TODO -> Check here.
this._observeQueue.drain();
if (callback) {
Meteor.defer(() => {

View File

@@ -621,7 +621,113 @@ Object.assign(Mongo.Collection.prototype, {
// generating their result until the database has acknowledged
// them. In the future maybe we should provide a flag to turn this
// off.
async insertAsync(doc) {
_insert(doc, callback) {
// Make sure we were passed a document to insert
if (!doc) {
throw new Error('insert requires an argument');
}
// Make a shallow clone of the document, preserving its prototype.
doc = Object.create(
Object.getPrototypeOf(doc),
Object.getOwnPropertyDescriptors(doc)
);
if ('_id' in doc) {
if (
!doc._id ||
!(typeof doc._id === 'string' || doc._id instanceof Mongo.ObjectID)
) {
throw new Error(
'Meteor requires document _id fields to be non-empty strings or ObjectIDs'
);
}
} else {
let generateId = true;
// Don't generate the id if we're the client and the 'outermost' call
// This optimization saves us passing both the randomSeed and the id
// Passing both is redundant.
if (this._isRemoteCollection()) {
const enclosing = DDP._CurrentMethodInvocation.get();
if (!enclosing) {
generateId = false;
}
}
if (generateId) {
doc._id = this._makeNewID();
}
}
// On inserts, always return the id that we generated; on all other
// operations, just return the result from the collection.
var chooseReturnValueFromCollectionResult = function(result) {
if (Meteor._isPromise(result)) return result;
if (doc._id) {
return doc._id;
}
// XXX what is this for??
// It's some iteraction between the callback to _callMutatorMethod and
// the return value conversion
doc._id = result;
return result;
};
const wrappedCallback = wrapCallback(
callback,
chooseReturnValueFromCollectionResult
);
if (this._isRemoteCollection()) {
const result = this._callMutatorMethod('insert', [doc], wrappedCallback);
return chooseReturnValueFromCollectionResult(result);
}
// it's my collection. descend into the collection object
// and propagate any exception.
try {
// If the user provided a callback and the collection implements this
// operation asynchronously, then queryRet will be undefined, and the
// result will be returned through the callback instead.
let result;
if (!!wrappedCallback) {
this._collection.insert(doc, wrappedCallback);
} else {
// If we don't have the callback, we assume the user is using the promise.
// We can't just pass this._collection.insert to the promisify because it would lose the context.
result = this._collection.insert(doc);
}
return chooseReturnValueFromCollectionResult(result);
} catch (e) {
if (callback) {
callback(e);
return null;
}
throw e;
}
},
/**
* @summary Insert a document in the collection. Returns its unique _id.
* @locus Anywhere
* @method insert
* @memberof Mongo.Collection
* @instance
* @param {Object} doc The document to insert. May not yet have an _id attribute, in which case Meteor will generate one for you.
* @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the _id as the second.
*/
insert(doc, callback) {
return this._insert(doc, callback);
},
async _insertAsync(doc) {
// Make sure we were passed a document to insert
if (!doc) {
throw new Error('insert requires an argument');
@@ -698,18 +804,17 @@ Object.assign(Mongo.Collection.prototype, {
}
},
// /**
// * @summary Insert a document in the collection. Returns its unique _id.
// * @locus Anywhere
// * @method insert
// * @memberof Mongo.Collection
// * @instance
// * @param {Object} doc The document to insert. May not yet have an _id attribute, in which case Meteor will generate one for you.
// * @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the _id as the second.
// */
// insertAsync(doc, callback) {
// return this._insertAsync(doc, callback);
// },
/**
* @summary Insert a document in the collection. Returns a promise that will return the document's unique _id when solved.
* @locus Anywhere
* @method insert
* @memberof Mongo.Collection
* @instance
* @param {Object} doc The document to insert. May not yet have an _id attribute, in which case Meteor will generate one for you.
*/
insertAsync(doc) {
return this._insertAsync(doc);
},
/**
* @summary Modify one or more documents in the collection. Returns the number of matched documents.
@@ -723,7 +828,6 @@ Object.assign(Mongo.Collection.prototype, {
* @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default).
* @param {Boolean} options.upsert True to insert a document if no matching documents are found.
* @param {Array} options.arrayFilters Optional. Used in combination with MongoDB [filtered positional operator](https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/) to specify which elements to modify in an array field.
* @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second.
*/
async updateAsync(selector, modifier, ...optionsAndCallback) {
@@ -773,7 +877,83 @@ Object.assign(Mongo.Collection.prototype, {
},
/**
* @summary Remove documents from the collection
* @summary Asynchronously modifies one or more documents in the collection. Returns the number of matched documents.
* @locus Anywhere
* @method update
* @memberof Mongo.Collection
* @instance
* @param {MongoSelector} selector Specifies which documents to modify
* @param {MongoModifier} modifier Specifies how to modify the documents
* @param {Object} [options]
* @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default).
* @param {Boolean} options.upsert True to insert a document if no matching documents are found.
* @param {Array} options.arrayFilters Optional. Used in combination with MongoDB [filtered positional operator](https://docs.mongodb.com/manual/reference/operator/update/positional-filtered/) to specify which elements to modify in an array field.
* @param {Function} [callback] Optional. If present, called with an error object as the first argument and, if no error, the number of affected documents as the second.
*/
update(selector, modifier, ...optionsAndCallback) {
const callback = popCallbackFromArgs(optionsAndCallback);
// We've already popped off the callback, so we are left with an array
// of one or zero items
const options = { ...(optionsAndCallback[0] || null) };
let insertedId;
if (options && options.upsert) {
// set `insertedId` if absent. `insertedId` is a Meteor extension.
if (options.insertedId) {
if (
!(
typeof options.insertedId === 'string' ||
options.insertedId instanceof Mongo.ObjectID
)
)
throw new Error('insertedId must be string or ObjectID');
insertedId = options.insertedId;
} else if (!selector || !selector._id) {
insertedId = this._makeNewID();
options.generatedId = true;
options.insertedId = insertedId;
}
}
selector = Mongo.Collection._rewriteSelector(selector, {
fallbackId: insertedId,
});
const wrappedCallback = wrapCallback(callback);
if (this._isRemoteCollection()) {
const args = [selector, modifier, options];
return this._callMutatorMethod('update', args);
}
// it's my collection. descend into the collection object
// and propagate any exception.
// If the user provided a callback and the collection implements this
// operation asynchronously, then queryRet will be undefined, and the
// result will be returned through the callback instead.
//console.log({callback, options, selector, modifier, coll: this._collection});
try {
// If the user provided a callback and the collection implements this
// operation asynchronously, then queryRet will be undefined, and the
// result will be returned through the callback instead.
return this._collection.update(
selector,
modifier,
options,
wrappedCallback
);
} catch (e) {
if (callback) {
callback(e);
return null;
}
throw e;
}
},
/**
* @summary Asynchronously removes documents from the collection.
* @locus Anywhere
* @method remove
* @memberof Mongo.Collection
@@ -792,6 +972,27 @@ Object.assign(Mongo.Collection.prototype, {
return this._collection.removeAsync(selector);
},
/**
* @summary Remove documents from the collection
* @locus Anywhere
* @method remove
* @memberof Mongo.Collection
* @instance
* @param {MongoSelector} selector Specifies which documents to remove
*/
remove(selector) {
selector = Mongo.Collection._rewriteSelector(selector);
if (this._isRemoteCollection()) {
return this._callMutatorMethod('remove', [selector]);
}
// it's my collection. descend into the collection1 object
// and propagate any exception.
return this._collection.remove(selector);
},
// Determine if this collection is simply a minimongo representation of a real
// database on another server
_isRemoteCollection() {
@@ -800,7 +1001,7 @@ Object.assign(Mongo.Collection.prototype, {
},
/**
* @summary Modify one or more documents in the collection, or insert one if no matching documents were found. Returns an object with keys `numberAffected` (the number of documents modified) and `insertedId` (the unique _id of the document that was inserted, if any).
* @summary Asynchronously modifies one or more documents in the collection, or insert one if no matching documents were found. Returns an object with keys `numberAffected` (the number of documents modified) and `insertedId` (the unique _id of the document that was inserted, if any).
* @locus Anywhere
* @method upsert
* @memberof Mongo.Collection
@@ -821,10 +1022,32 @@ Object.assign(Mongo.Collection.prototype, {
});
},
/**
* @summary Modify one or more documents in the collection, or insert one if no matching documents were found. Returns an object with keys `numberAffected` (the number of documents modified) and `insertedId` (the unique _id of the document that was inserted, if any).
* @locus Anywhere
* @method upsert
* @memberof Mongo.Collection
* @instance
* @param {MongoSelector} selector Specifies which documents to modify
* @param {MongoModifier} modifier Specifies how to modify the documents
* @param {Object} [options]
* @param {Boolean} options.multi True to modify all matching documents; false to only modify one of the matching documents (the default).
*/
upsert(selector, modifier, options) {
return this.update(
selector,
modifier,
{
...options,
_returnObject: true,
upsert: true,
});
},
// We'll actually design an index API later. For now, we just pass through to
// Mongo's, but make it synchronous.
/**
* @summary Creates the specified index on the collection.
* @summary Asynchronously creates the specified index on the collection.
* @locus server
* @method ensureIndexAsync
* @deprecated in 3.0
@@ -851,7 +1074,7 @@ Object.assign(Mongo.Collection.prototype, {
},
/**
* @summary Creates the specified index on the collection.
* @summary Asynchronously creates the specified index on the collection.
* @locus server
* @method createIndexAsync
* @memberof Mongo.Collection

View File

@@ -23,7 +23,7 @@ ObserveMultiplexer = class {
const self = this;
this.callbackNames().forEach(callbackName => {
this[callbackName] = function(/* ... */) {
self._applyCallback(callbackName, _.toArray(arguments));
return self._applyCallback(callbackName, _.toArray(arguments));
};
});
}