mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Sync mongo methods for the client again
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,15 @@ Ledger.allow({
|
||||
removeAsync: function() {
|
||||
return true;
|
||||
},
|
||||
insert: function() {
|
||||
return true;
|
||||
},
|
||||
update: function() {
|
||||
return true;
|
||||
},
|
||||
remove: function() {
|
||||
return true;
|
||||
},
|
||||
fetch: []
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +155,6 @@ export default class LocalCollection {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO -> Check here.
|
||||
this._observeQueue.drain();
|
||||
if (callback) {
|
||||
Meteor.defer(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user