diff --git a/History.md b/History.md index 92e2b93f06..0378f1b54d 100644 --- a/History.md +++ b/History.md @@ -40,6 +40,9 @@ * Meteor now provides a compatible replacement for the DOM `localStorage` facility that works in IE7, in the `localstorage-polyfill` smart package. +* Meteor now packages the D3 library for manipulating documents based on data in + a smart package called `d3`. + * `Meteor.Collection` now takes its optional `manager` argument (used to associate a collection with a server you've connected to with `Meteor.connect`) as a named option. (The old call syntax continues to work diff --git a/LICENSE.txt b/LICENSE.txt index 5f5319ca24..201b3c924c 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -356,6 +356,11 @@ Copyright (c) 2011: Tim Koschützki (tim@debuggable.com) Felix Geisendörfer (felix@debuggable.com) +---------- +node-form-data: https://github.com/felixge/node-form-data +---------- + +Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors ============== @@ -622,6 +627,11 @@ npmlog: https://github.com/isaacs/npmlog once: https://github.com/isaacs/once osenv: https://github.com/isaacs/osenv mute-stream: https://github.com/isaacs/mute-stream +couch-login: https://github.com/isaacs/couch-login +npmconf: https://github.com/isaacs/npmconf +read-installed: https://github.com/isaacs/read-installed +read-package-json: https://github.com/isaacs/read-package-json +promzard: https://github.com/isaacs/promzard ---------- Copyright (c) Isaac Z. Schlueter ("Author") @@ -752,6 +762,38 @@ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISE OF THE POSSIBILITY OF SUCH DAMAGE. +---------- +D3: http://d3js.org/ +---------- + +Copyright (c) 2012, Michael Bostock +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ============= Public Domain @@ -841,6 +883,36 @@ By Isaac Z. Schlueter (http://blog.izs.me/) 0. You just DO WHAT THE FUCK YOU WANT TO. +---------- +node-stream-buffer: https://github.com/samcday/node-stream-buffer +---------- + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + + ---------- mongodb: http://www.mongodb.org/ ---------- diff --git a/docs/client/api.html b/docs/client/api.html index faf6db0870..712767f7c7 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -3,10 +3,10 @@

The Meteor API

-Your Javascript code can run in two environments: the *client* -(browser), and the *server* (a Node.js container on a server). For -each function in this API reference, we'll indicate if the function is -available just on the client, just on the server, or *Anywhere*. +Your Javascript code can run in two environments: the *client* (browser), and +the *server* (a [Node.js](http://nodejs.org/) container on a server). For each +function in this API reference, we'll indicate if the function is available just +on the client, just on the server, or *Anywhere*.

Meteor Core

@@ -47,7 +47,14 @@ will publish that cursor's documents. // server: publish the rooms collection, minus secret info. Meteor.publish("rooms", function () { - return Rooms.find({}, {fields: {secretInfo: false}}); + return Rooms.find({}, {fields: {secretInfo: 0}}); + }); + + // ... and publish secret info for rooms where the logged-in user + // is an admin. If the client subscribes to both streams, the records + // are merged together into the same documents in the Rooms collection. + Meteor.publish("adminSecretInfo", function () { + return Rooms.find({admin: this.userId}, {fields: {secretInfo: 1}}); }); Otherwise, the publish function can [`set`](#publish_set) and @@ -108,6 +115,11 @@ project that includes the `autopublish` package. Your publish function will still work. {{/warning}} +{{> api_box subscription_userId}} + +This is constant. However, if the logged-in user changes, the publish +function is rerun with the new value. + {{> api_box subscription_set}} {{> api_box subscription_unset}} {{> api_box subscription_complete}} @@ -120,11 +132,6 @@ is the place to stop the observes. {{> api_box subscription_stop}} -{{> api_box subscription_userId}} - -This is constant. However, if the logged-in user changes, the publish -function is rerun with the new value. - {{> api_box subscribe}} When you subscribe to a record set, it tells the server to send records @@ -150,9 +157,9 @@ attribute.) If all of the attributes in a document are removed, Meteor will remove the (now empty) document. If you want to publish empty -documents, just use a placeholder attribute. +documents, just use a placeholder attribute: - // Clicks.insert({exists: true}); + Clicks.insert({exists: true}); {{> api_box autosubscribe}} @@ -206,7 +213,7 @@ object, which provides the following: * `isSimulation`: a boolean value, true if this invocation is a stub. * `unblock`: when called, allows the next method from this client to begin running. -* `userId`: a function that returns the id of the current user. +* `userId`: the id of the current user. * `setUserId`: a function that associates the current client with a user. Calling `methods` on the client defines *stub* functions associated with @@ -225,9 +232,9 @@ exception it will be logged to the console. {{> api_box method_invocation_userId}} -The user id is an arbitrary string — typically the id of the user -record in the database. You can set it with the `setUserId` function. If -you're using the Meteor accounts system then this is handled for you. +The user id is an arbitrary string — typically the id of the user record +in the database. You can set it with the `setUserId` function. If you're using +the [Meteor accounts system](#accounts_api) then this is handled for you. {{> api_box method_invocation_setUserId}} @@ -236,9 +243,9 @@ connection that made this method call. This simply sets the value of `userId` for future method calls received on this connection. Pass `null` to log out the connection. -If you are using the built-in Meteor accounts system then this should correspond -to the `_id` field of a document in the [`Meteor.users`](#meteor_users) -collection. +If you are using the [built-in Meteor accounts system](#accounts_api) then this +should correspond to the `_id` field of a document in the +[`Meteor.users`](#meteor_users) collection. `setUserId` is not retroactive. It affects the current method call and any future method calls on the connection. Any previous method calls on @@ -268,7 +275,7 @@ This is how to invoke a method. It will run the method on the server. If a stub is available, it will also run the stub on the client. If you include a callback function as the last argument (which can't be -an argument to the method, since functions aren't serializeable), the +an argument to the method, since functions aren't serializable), the method will run asynchronously: it will return nothing in particular and will not throw an exception. When the method is complete (which may or may not happen before `Meteor.call` returns), the callback will be @@ -312,8 +319,9 @@ only to the server.) {{> api_box meteor_apply}} -`Meteor.apply` is just like `Meteor.call`, but it allows the -arguments to be passed as an array. +`Meteor.apply` is just like `Meteor.call`, except that the method arguments are +passed as an array rather than directly as arguments, and you can specify +options about how the client executes the method.

Server connections

@@ -382,7 +390,7 @@ sets, call `Meteor.connect` with the URL of the application. Get the current connection status. See [Meteor.status](#meteor_status). * `reconnect` - - See Meteor.reconnect. + See [Meteor.reconnect](#meteor_reconnect). * `onReconnect` - Set this to a function to be called as the first step of reconnecting. This function can call methods which will be executed before any other outstanding methods. For example, this can be used to re-establish @@ -444,7 +452,8 @@ Specifically, when you pass a `name`, here's what happens: * On the server, a collection with that name is created on a backend Mongo server. When you call methods on that collection on the server, -they translate directly into normal Mongo operations. +they translate directly into normal Mongo operations (after checking that +they match your [access control rules](#allow)). * On the client, a Minimongo instance is created. Minimongo is essentially an in-memory, non-persistent @@ -471,13 +480,8 @@ the package: $ meteor remove autopublish -{{#warning}} -Currently the client is given full write access to the collection. They -can execute arbitrary Mongo update commands. Once we build -authentication, you will be able to limit the client's direct access to -insert, update, and remove. We are also considering validators and -other ORM-like functionality. -{{/warning}} +and instead call [`Meteor.publish`](#meteor_publish) to specify which parts of +your collection should be published to which users. // Create a collection called Posts and put a document in it. The // document will be immediately visible in the local copy of the @@ -546,8 +550,8 @@ those changes may or may not appear in the result set. Cursors are a reactive data source. The first time you retrieve a cursor's documents with `fetch`, `map`, or `forEach` inside a -reactive context (eg, [`Meteor.render`](#meteor_render), -[`Meteor.autosubscribe`](#meteor_autosubscribe), Meteor will register a +reactive context (eg, [`Meteor.render`](#meteor_render) or +[`Meteor.autosubscribe`](#meteor_autosubscribe)), Meteor will register a dependency on the underlying data. Any change to the collection that changes the documents in a cursor will trigger a recomputation. To disable this behavior, pass `{reactive: false}` as an option to @@ -589,10 +593,9 @@ Example: {{> api_box update}} -Modify documents that match `selector` as -given by `modifier` (see modifier -documentation). By default, modify only one matching document. -If `multi` is true, modify all matching documents. +Modify documents that match `selector` as given by `modifier` (see [modifier +documentation](#modifiers)). By default, modify only one matching document. If +`multi` is true, modify all matching documents. Instead of a selector, you can pass a string, which will be interpreted as an `_id`. @@ -656,7 +659,7 @@ Example: {{> api_box allow}} When a client calls `insert`, `update`, or `remove` on a collection, the -collection's `allow` and `deny` callbacks are called +collection's `allow` and [`deny`](#deny) callbacks are called on the server to determine if the write should be allowed. If at least one `allow` callback allows the write, and no `deny` callbacks deny the write, then the write is allowed to proceed. @@ -718,7 +721,7 @@ actually needed by your functions. This is enabled by the `fetch` option. Set `fetch` to an array of the field names that should be retrieved. -Example: XXX test me! +Example: // Create a collection where users can only modify documents that // they own. Ownership is tracked by an 'owner' field on each @@ -772,12 +775,12 @@ having the clients call `insert`, `update`, and `remove` directly on the collection. Meteor also has a special "insecure mode" for quickly prototyping new -applications. In insecure mode, if you haven't set up any `allow` or -`deny` rules on a collection, then all users have full write access to -the collection. This is the only effect of insecure mode. If you call -`allow` or `deny` at all, even `allow({})`, then access is checked just -like normal. __New Meteor projects start in insecure mode by default.__ To -turn it off just type `meteor remove insecure`. +applications. In insecure mode, if you haven't set up any `allow` or `deny` +rules on a collection, then all users have full write access to the +collection. This is the only effect of insecure mode. If you call `allow` or +`deny` at all on a collection, even `Posts.allow({})`, then access is checked +just like normal on that collection. __New Meteor projects start in insecure +mode by default.__ To turn it off just type `meteor remove insecure`. {{#note}} For `update` and `remove`, documents will be affected only if they match @@ -790,7 +793,7 @@ and checked by allow and deny)]}}]}`. {{> api_box deny}} -This works just like `allow`, except it lets you +This works just like [`allow`](#allow), except it lets you make sure that certain writes are definitely denied, even if there is an `allow` rule that says that they should be permitted. @@ -943,7 +946,8 @@ But they can also contain more complicated tests: // Matches documents where fruit is one of three possibilities {fruit: {$in: ["peach", "plum", "pear"]}} -See the complete documentation. +See the [complete +documentation](http://www.mongodb.org/display/DOCS/Advanced+Queries). {{/api_box_inline}} @@ -959,14 +963,17 @@ place by changing some of its fields. Some examples: // 'supporters' array {$inc: {votes: 2}, $push: {supporters: "Traz"}} -But if a modifier doesn't contain any $-operators, then it is -instead interpreted as a literal document, and completely replaces -whatever was previously in the database. +But if a modifier doesn't contain any $-operators, then it is instead +interpreted as a literal document, and completely replaces whatever was +previously in the database. (Literal document modifiers are not currently +supported by [validated updates](#allow).) // Find the document with id "123", and completely replace it. Users.update({_id: "123"}, {name: "Alice", friends: ["Bob"]}); -See the full list of modifiers. +See the [full list of +modifiers](http://www.mongodb.org/display/DOCS/Updating#Updating-ModifierOperations) +full list of modifiers. {{/api_box_inline}} @@ -1101,6 +1108,359 @@ For object and array session values, you cannot use `Session.equals`; instead, you need to use the `underscore` package and write `_.isEqual(Session.get(key), value)`. + + +

Accounts

+ +XXX intro text + +{{> api_box user}} + +Retreives the user record for the current user from +the [`Meteor.users`](#meteor_users) collection. + +On the client this will be a subset of the fields in the document, only +those that are published from the server are available on the client. By +default the server publishes `username`, `emails`, and +`profile`. See [`Meteor.users`](#meteor_users) for more on +the fields used in user documents. + +If the user is logged in but the user's database record is not fully +loaded yet, this returns an object with only the `_id` field set. During +this period [`userLoaded`](#meteor_userloaded) will return +`false`. + +{{> api_box userId}} + +{{> api_box users}} + +This collection contains one document per registered user. Here's an example +user document: + + { + _id: "bbca5d6a-2156-41c4-89da-0329e8c99a4f", // Meteor.userId() + username: "cool_kid_13", // unique name + emails: [ + // each email address can only belong to one user. + { address: "cool@example.com", verified: true }, + { address: "another@different.com", verified: false } + ], + profile: { + // The profile is writable by the user by default. + name: "Joe Schmoe" + }, + services: { + facebook: { + id: "709050", // facebook id + accessToken: "AAACCgdX7G2...AbV9AZDZD" + }, + resume: { + loginTokens: [ + { token: "97e8c205-c7e4-47c9-9bea-8e2ccc0694cd", + when: 1349761684048 } + ] + } + } + } + +A user document can contain any data you want to store about a user. Meteor +treats the following fields specially: + +- `username`: a unique String identifying the user. +- `emails`: an Array of Objects with keys `address` and `verified`; + an email address may belong to at most one user. `verified` is + a Boolean which is true if the user has [verified the + address](#accounts_verifyemail) with a token sent over email. +- `profile`: an Object which (by default) the user can create + and update with any data. +- `services`: an Object containing data used by particular + login services. For example, its `reset` field contains + tokens used by [forgot password](#accounts_forgotpassword) links, + and its `resume` field contains tokens used to keep you + logged in between sessions. + +Like all [Meteor.Collection](#collections)s, you can access all +documents on the server, but only those specifically published by the server are +available on the client. + +By default, the current user's `username`, `emails` and `profile` are +published to the client. You can publish additional fields for the +current user with: + + Meteor.publish("userData", function () { + return Meteor.users.find({_id: this.userId}, + {fields: {'other': 1, 'things': 1}}); + }); + +If the `autopublish` package is installed, the `username` and `profile` fields +for all users are published to all clients. To publish specific fields from all +users: + + Meteor.publish("allUserData", function () { + return Meteor.users.find({}, {fields: {'nested.things': 1}}); + }); + +Users are by default allowed to specify their own `profile` field with +[`Accounts.createUser`](#accounts_createuser) and modify it with +`Meteor.users.update`. To allow users to edit additional fields, use +[`Meteor.users.allow`](#allow). To forbid users from making any modifications to +their user document: + + Meteor.users.deny({update: function () { return true; }}); + + +{{> api_box userLoaded}} + +There are some cases when the client knows the id of the logged in user +but has not yet received the user data from the server. For example, if +the user is logged in and reloads the page the user data will be +unavailable during initial page load. + +During these periods, `userLoaded` will return false +and [`user`](#meteor_user) will return an object with only +the `_id` key. + +{{#note}} +We realize this is inconvenient. It is a temporary solution. In the +future we will either make it unnecessary or fold it into a more +general mechanism. +{{/note}} + +{{> api_box logout}} + +{{> api_box loginWithPassword}} + +XXX needs link to passwords section and mention the package + +{{> api_box loginWithExternalService}} + +These functions initiate the login process with an external +service (eg: Facebook, Google, etc), using OAuth. When called they open a new pop-up +window that loads the provider's login page. Once the user has logged in +with the provider, the pop-up window is closed and the Meteor client +logs in to the Meteor server with the information provided by the external +service. + + +If the user has not already granted all the permissions requested they will be +prompted to grant access to their account in the pop-up dialog. Values for the +`requestPermissions` parameter differ for each login service: + +- Facebook: +- GitHub: +- Google: +- Twitter, Weibo: `requestPermissions` currently not supported + +XXX mention provider packages + +{{> api_box accounts_config}} +{{> api_box accounts_ui_config}} + +Example: + + Accounts.ui.config({ + requestPermissions: { + facebook: ['user_likes'], + github: ['user', 'repo'] + }, + passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL' + }); + +{{> api_box accounts_validateNewUser}} + +This can be called multiple times. If any of the functions return `false` or +throw an error, the new user creation is aborted. To set a specific error +message (which will be displayed by [`accounts-ui`](#accountsui)), throw a new +[`Meteor.Error`](#meteor_error). + +Example: + + // Validate username, sending a specific error message on failure. + Accounts.validateNewUser(function (user) { + if (user.username && user.username.length >= 3) + return true; + throw new Meteor.Error(403, "Username must have at least 3 characters"); + }); + // Validate username, without a specific error message. + Accounts.validateNewUser(function (user) { + return user.username !== "root"; + }); + +{{> api_box accounts_onCreateUser}} + +Use this when you need to do more than simply accept or reject new user +creation. With this function you can programatically control the +contents of new user documents. + +The function you pass will be called with two arguments: `options` and +`user`. The `options` argument comes +from [`Accounts.createUser`](#accounts_createuser) for +password-based users or from an external service login flow. `options` may come +from an untrusted client so make sure to validate any values you read from +it. The `user` argument is created on the server and contains a +proposed user object with all the automatically generated fields +required for the user to log in. + +The function should return the user document (either the one passed in or a +newly-created object) with whatever modifications are desired. The returned +document is inserted directly into the [`Meteor.users`](#meteor_users) collection. + +The default create user function simply copies `options.profile` into +the new user document. Calling `onCreateUser` overrides the default +hook. This can only be called once. + +Example: + + + + // Support for playing D&D: Roll 3d6 for dexterity + Accounts.onCreateUser(function(options, user) { + var d6 = function () { return Math.floor(Math.random() * 6) + 1; }; + user.dexterity = d6() + d6() + d6(); + // We still want the default hook's 'profile' behavior. + if (options.profile) + user.profile = options.profile; + return user; + }); + + +

Passwords

+ +The `accounts-password` package implements a complete system for +password based authentication. In addition to the basic username and +password based sign-in process it also supports email based sign-in +including address verification and password recovery emails. + +Unlike most web applications, the Meteor client does not send the user's +password directly to the server. It uses the [Secure Remote Password +protocol](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) +to ensure the server never sees the user's plain-text password. This +helps protect against embarrassing password leaks if the server's +database is compromised. + +To add password support to your application, run `$ meteor add +accounts-password`. You can construct your own user interface using the +functions below, or use the [`accounts-ui` package](#accountsui) to +include a turn-key user interface for password-based sign-in. + + +{{> api_box accounts_createUser}} + +On the client this function logs in as the newly created user on +successful completion. On the server, it returns the newly created user +id. + +On the client, you must pass `password` and one of `username` or `email` +— enough information for the user to be able to log in again +later. On the server, you can pass any subset of these options, but the +user will not be able to log in until it has an identifier and a +password. + +To create an account without a password on the server and still let the +user pick their own password, call `createUser` with the `email` option +and then +call [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail). This +will send the user an email with a link to set their initial password. + +By default the `profile` option is added directly to the new user document. To +override this behavior, use [`Accounts.onCreateUser`](#accounts_createuser). + +This function is only used for creating users with passwords. The external +service login flows do not use this function. + + +{{> api_box accounts_changePassword}} + +{{> api_box accounts_forgotPassword}} + +This triggers a call +to [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail) +on the server. Pass the token the user receives in this email +to [`Accounts.resetPassword`](#accounts_resetpassword) to +complete the password reset process. + +If you are using the [`accounts-ui` package](#pkg_accounts_ui), this is handled +automatically. Otherwise, it is your responsiblity to prompt the user for the +new password and call `resetPassword`. + +{{> api_box accounts_resetPassword}} + +This function accepts tokens generated +by [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail) +and +[`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail). + +{{> api_box accounts_setPassword}} + +{{> api_box accounts_verifyEmail}} + +This function accepts tokens generated +by [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail). It +sets the `emails.verified` field in the user record. + +{{> api_box accounts_sendResetPasswordEmail}} + +The token in this email should be passed +to [`Accounts.resetPassword`](#accounts_resetpassword). + +To customize the contents of the email, see +[`Accounts.emailTemplates`](#accounts_emailtemplates). + +{{> api_box accounts_sendEnrollmentEmail}} + +The token in this email should be passed +to [`Accounts.resetPassword`](#accounts_resetpassword). + +To customize the contents of the email, see +[`Accounts.emailTemplates`](#accounts_emailtemplates). + +{{> api_box accounts_sendVerificationEmail}} + +The token in this email should be passed +to [`Accounts.verifyEmail`](#accounts_verifyemail). + +To customize the contents of the email, see +[`Accounts.emailTemplates`](#accounts_emailtemplates). + +{{> api_box accounts_emailTemplates}} + +This is an `Object` with several fields that are used to generate text +for the emails by `sendResetPasswordEmail`, `sendEnrollmentEmail`, and +`sendVerificationEmail`. + +Override fields of the object by assigning to them: + +- `from`: A `String` with an [RFC5322](http://tools.ietf.org/html/rfc5322) From + address. By default email is from `no-reply@meteor.com`. If you wish to + receive email from users asking for help with their account, be sure to set + this to an email address that you can receive email at. +- `siteName`: The public name of your application. Defaults to the DNS name of + the application (eg: `awesome.meteor.com`). +- `resetPassword`: An `Object` with two fields: + - `resetPassword.subject`: A `Function` that takes a user object and returns + a `String` for the subject line of a reset password email. + - `resetPassword.text`: A `Function` that takes a user object and a url, and + returns the body text for a reset password email. +- `enrollAccount`: Same as `resetPassword`, but for initial password setup for + new accounts. +- `verifyEmail`: Same as `resetPassword`, but for verifying the users email + address. + + +Example: + + Accounts.emailTemplates.siteName = "AwesomeSite"; + Accounts.emailTemplates.from = "AwesomeSite Admin "; + Accounts.emailTemplates.enrollAccount.subject = function (user) { + return "Welcome to Awesome Town, " + user.profile.name; + }; + Accounts.emailTemplates.enrollAccount.text = function (user, url) { + return "You have been selected to participate in building a better future!" + + " To activate your account, simply click the link below:\n\n" + + url; + }; + +

Templates

A template that you declare as `<{{! }}template name="foo"> ... Accounts - -XXX intro text - -{{> api_box user}} - -Retreives the user record for the current user from -the [`Meteor.users`](#meteor_users) collection. - -On the client this will be a subset of the fields in the document, only -those that are published from the server are available on the client. By -default the server publishes `username`, `emails`, and -`profile`. See [`Meteor.users`](#meteor_users) for more on -the fields used user documents. - -If the user is logged in but the user's database record is not fully -loaded yet, this returns an object with only the `_id` field set. During -this period [`userLoaded`](#meteor_userloaded) will return -`false`. - -{{> api_box userId}} - -{{> api_box users}} - -This collection contains one document per user. Example user document: - - { - _id: "bbca5d6a-2156-41c4-89da-0329e8c99a4f" // userId - username: "cool_kid_13", // unique name - emails: [ - // each email address can only belong to one user. - { address: "cool@example.com", verified: true }, - { address: "another@different.com", verified: false } - ], - profile: { - // The profile is writable by the user by default. - name: "Joe Schmoe" - }, - services: { - facebook: { - id: "709050", // facebook id - accessToken: "AAACCgdX7G2...AbV9AZDZD" - }, - resume: { - loginTokens: [ - { token: "97e8c205-c7e4-47c9-9bea-8e2ccc0694cd", - when: 1349761684048 } - ] - } - } - } - - -Like all
Meteor.Collections, you can access all -documents on the server, but only those specifically published by the server are -available on the client. - -By default, the current user's `username`, `emails` and `profile` are -published to the client. You can publish additional fields for the -current user with: - - Meteor.publish("userData", function () { - return Meteor.users.find({_id: this.userId}, - {fields: {'other': 1, 'things': 1}}); - }); - -If the `autopublish` package is installed, the `username` and `profile` fields -for all users are published to all clients. To publish specific fields from all -users: - - Meteor.publish("allUserData", function () { - return Meteor.users.find({}, {fields: {'nested.things': 1}}); - }); - -Users are by default allowed to specify their own `profile` field with -[`Accounts.createUser`](#accounts_createuser) and modify it with -`Meteor.users.update`. To allow users to edit additional fields, use -[`Meteor.users.allow`](#allow). To forbid users from making any modifications to -their user document: - - Meteor.users.deny({update: function () { return true; }}); - - -{{> api_box userLoaded}} - -There are some cases when the client knows the id of the logged in user -but has not yet received the user data from the server. For example, if -the user is logged in and reloads the page the user data will be -unavailable during initial page load. - -During these periods, `userLoaded` will return false -and [`user`](#meteor_user) will return an object with only -the `_id` key. - -{{#note}} -We realize this is inconvenient. It is a temporary solution. In the -future we will either make it unnecessary or fold it into a more -general mechanism. -{{/note}} - -{{> api_box logout}} - -{{> api_box loginWithPassword}} - -{{> api_box loginWithOAuth}} - -These functions initiate the login process with a third party OAuth -provider (eg: Facebook, Google, etc). When called they open a new pop-up -window that loads the provider's login page. Once the user has logged in -with the provider, the pop-up window is closed and the Meteor client -logs in to the Meteor server with the information provided by the OAuth -provider. - -If the user has not already granted all the permissions requested they will be -prompted to grant access to their account in the pop-up dialog. Values for the -`requestPermissions` parameter differ for each login service: - -- Facebook: http://developers.facebook.com/docs/authentication/permissions/ -- GitHub: http://developer.github.com/v3/oauth/#scopes -- Google: https://developers.google.com/accounts/docs/OAuth2Login#scopeparameter - -Currently, `loginWithTwitter` and `loginWithWeibo` do not support -`requestPermissions`. - - -{{> api_box accounts_createUser}} - -On the client this function logs in as the newly created user on -successful completion. On the server, it returns the newly created user -id. - -On the client, you must pass `password` and one of `username` or `email` -— enough information for the user to be able to log in again -later. On the server, you can pass any subset of these options, but the -user will not be able to log in until it has an identifier and a -password. - -To create an account without a password on the server and still let the -user pick their own password, call `createUser` with the `email` option -and then -call [`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail). This -will send the user an email with a link to set their initial password. - -By default the `profile` option is added directly to the new user document. To -override this behavior, use [`Accounts.onCreateUser`](#accounts_createuser). - -This function is only used for creating users with passwords. The OAuth -login flows do not use this function. - - -{{> api_box accounts_changePassword}} - -{{> api_box accounts_forgotPassword}} - -This triggers a call -to [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail) -on the server. Pass the token the user receives in this email -to [`Accounts.resetPassword`](#accounts_resetpassword) to -complete the password reset process. - -If you are using the `accounts-ui` - package, this is handled automatically. Otherwise, it is your -responsiblity to prompt the user for the new password and call `resetPassword`. - -- XXX token goes to `Accounts._resetPasswordToken`. - -{{> api_box accounts_resetPassword}} - -This function accepts tokens generated -by [`Accounts.sendResetPasswordEmail`](#accounts_sendresetpasswordemail) -and -[`Accounts.sendEnrollmentEmail`](#accounts_sendenrollmentemail) - -{{> api_box accounts_setPassword}} - -{{> api_box accounts_verifyEmail}} - -This function accepts tokens generated -by [`Accounts.sendVerificationEmail`](#accounts_sendverificationemail). It -sets the `emails.verified` field in the user record. - -{{> api_box accounts_sendResetPasswordEmail}} - -The token in this email should be passed -to [`Accounts.resetPassword`](#accounts_resetpassword). - -To customize the contents of the email, see -[`Accounts.emailTemplates`](#accounts_emailtemplates). - -{{> api_box accounts_sendEnrollmentEmail}} - -The token in this email should be passed -to [`Accounts.resetPassword`](#accounts_resetpassword). - -To customize the contents of the email, see -[`Accounts.emailTemplates`](#accounts_emailtemplates). - -{{> api_box accounts_sendVerificationEmail}} - -The token in this email should be passed -to [`Accounts.verifyEmail`](#accounts_verifyemail). - -To customize the contents of the email, see -[`Accounts.emailTemplates`](#accounts_emailtemplates). - -{{> api_box accounts_emailTemplates}} - -This is an `Object` with several fields that are used to generate text -for the emails by `sendResetPasswordEmail`, `sendEnrollmentEmail`, and -`sendVerificationEmail`. - -Override fields of the object by assigning to them: - -- `from`: A `String` with an RFC5322 From address. By default email is from - `no-reply@meteor.com`. If you wish to receive email from users asking for help - with their account, be sure to set this to an email address that you can receive - email at. -- `siteName`: The public name of your application. Defaults to the DNS name of - the application (eg: `awesome.meteor.com`). -- `resetPassword`: An `Object` with two fields: - - `resetPassword.subject`: A `Function` that takes a user object and returns - a `String` for the subject line of a reset password email. - - `resetPassword.text`: A `Function` that takes a user object and a url, and - returns the body text for a reset password email. -- `enrollAccount`: Same as `resetPassword`, but for initial password setup for - new accounts. -- `verifyEmail`: Same as `resetPassword`, but for verifying the users email - address. - - -Example: - - Accounts.emailTemplates.siteName = "AwesomeSite"; - Accounts.emailTemplates.from = "AwesomeSite Admin "; - Accounts.emailTemplates.enrollAccount.subject = function (user) { - return "Welcome to Awesome Town, " + user.profile.name; - }; - Accounts.emailTemplates.enrollAccount.text = function (user, url) { - return "You have been selected to participate in building better future!" - + " To activate your account, simply click the link below:\n\n" - + url; - }; - - -{{> api_box accounts_config}} -{{> api_box accounts_ui_config}} - -{{> api_box accounts_validateNewUser}} - -This can be called multiple times. If any of the functions return -`false` the new user creation is aborted. - -Example: - - // All users must have a username longer than 3 characters. - Accounts.validateNewUser(function (user) { - return user.username && user.username.length >= 3; - }); - -{{> api_box accounts_onCreateUser}} - -Use this when you need to do more than simply accept or reject new user -creation. With this function you can programatically control the -contents of new user documents. - -The function you pass will be called with two arguments: `options` and -`user`. The `options` argument comes -from [`Accounts.createUser`](#accounts_createuser) for -password-based users or from the OAuth login flow. `options` may come -from an untrusted client so make to validate any values you read from -it. The `user` argument is created on the server and contains a -proposed user object with all the automatically generated fields -required for the user to log in. - -The function should return a new user object with whatever modifications -are desired. The returned object is inserted directly into -the [`Meteor.users`](#meteor_users) collection. - -The default create user function simply copies `options.profile` into -the new user document. Calling `onCreateUser` overrides the default -hook. This can only be called once. - -Example: - - // Make members of the GitHub 'meteor' group into admins. - Accounts.onCreateUser(function(options, user) { - // http://developer.github.com/v3/orgs/members/#get-member - if (!Meteor.http.get( - "https://api.github.com/orgs/meteor/members/" + - user.services.github.username + - "?access_token=" + user.services.github.accessToken - ).error) { - user.admin = true; - } - return user; - }); - -

Timers

Meteor uses global environment variables @@ -2297,9 +2357,9 @@ send mail. Currently, Meteor supports sending mail over SMTP; the `MAIL_URL` environment variable should be of the form `smtp://USERNAME:PASSWORD@HOST:PORT/`. For apps deployed with `meteor deploy`, `MAIL_URL` defaults to an account (provided by -Mailgun) which allows -apps to send up to 200 emails per day; you may override this default by -assigning to `process.env.MAIL_URL` before your first call to `Email.send`. +[Mailgun](http://www.mailgun.com/)) which allows apps to send up to 200 emails +per day; you may override this default by assigning to `process.env.MAIL_URL` +before your first call to `Email.send`. If `MAIL_URL` is not set (eg, when running your application locally), `Email.send` outputs the message to standard output instead. diff --git a/docs/client/api.js b/docs/client/api.js index 476a85c95c..0f339c1b9e 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -741,7 +741,7 @@ Template.api.loginWithPassword = { { name: "password", type: "String", - descr: "The user's password. This is __not__ sent in plain text over the wire — it is secured with SRP." + descr: "The user's password. This is __not__ sent in plain text over the wire — it is secured with [SRP](http://en.wikipedia.org/wiki/Secure_Remote_Password_protocol)." }, { name: "callback", @@ -752,11 +752,11 @@ Template.api.loginWithPassword = { }; -Template.api.loginWithOAuth = { - id: "meteor_loginwithoauth", - name: "Meteor.loginWithOAuthProvider([options], [callback])", +Template.api.loginWithExternalService = { + id: "meteor_loginwithexternalservice", + name: "Meteor.loginWithExternalService([options], [callback])", locus: "Client", - descr: ["Log the user in using an external OAuth service."], + descr: ["Log the user in using an external service."], args: [ { name: "callback", @@ -773,6 +773,76 @@ Template.api.loginWithOAuth = { ] }; + + +Template.api.accounts_config = { + id: "accounts_config", + name: "Accounts.config(options)", + locus: "Anywhere", + descr: ["Set global accounts options."], + options: [ + { + name: "sendVerificationEmail", + type: "Boolean", + descr: "New users with an email address will receive an address verification email." + }, + { + name: "forbidClientAccountCreation", + type: "Boolean", + descr: "[`createUser`](#accounts_createuser) requests from the client will be rejected." + } + ] +}; + +Template.api.accounts_ui_config = { + id: "accounts_ui_config", + name: "Accounts.ui.config(options)", + locus: "Client", + descr: ["Configure the behavior of [`{{loginButtons}}`](#accountsui)."], + options: [ + { + name: "requestPermissions", + type: "Object", + descr: "Which [permissions](#requestpermissions) to request from the user for each external service." + }, + { + name: "passwordSignupFields", + type: "String", + descr: "Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default)." + } + ] +}; + +Template.api.accounts_validateNewUser = { + id: "accounts_validatenewuser", + name: "Accounts.validateNewUser(func)", + locus: "Server", + descr: ["Set restrictions on new user creation."], + args: [ + { + name: "func", + type: "Function", + descr: "Called whenever a new user is created. Takes the new user object, and returns true to allow the creation or false to abort." + } + ] +}; + +Template.api.accounts_onCreateUser = { + id: "accounts_oncreateuser", + name: "Accounts.onCreateUser(func)", + locus: "Server", + descr: ["Customize new user creation."], + args: [ + { + name: "func", + type: "Function", + descr: "Called whenever a new user is created. Return the new user object, or throw an `Error` to abort the creation." + } + ] +}; + + + Template.api.accounts_createUser = { id: "accounts_createuser", name: "Accounts.createUser(options, [callback])", @@ -985,75 +1055,6 @@ Template.api.accounts_emailTemplates = { -Template.api.accounts_config = { - id: "accounts_config", - name: "Accounts.config(options)", - locus: "Anywhere", - descr: ["Set global accounts options."], - options: [ - { - name: "sendVerificationEmail", - type: "Boolean", - descr: "New users with an email address will receive an address verification email." - }, - { - name: "forbidClientAccountCreation", - type: "Boolean", - descr: "[`createUser`](#accounts_createuser) requests from the client will be rejected." - } - ] -}; - -Template.api.accounts_ui_config = { - id: "accounts_ui_config", - name: "Accounts.ui.config(options)", - locus: "Client", - descr: ["Set Accounts UI options for the `loginButtons` template."], - options: [ - { - name: "requestPermissions", - type: "Object", - descr: "Which permissions to request from the user for each OAuth service. For example: `{facebook: ['user_likes'], github: ['user', 'repo']}`" - }, - { - name: "passwordSignupFields", - type: "String", - descr: "Which fields to display in the user creation form. One of '`USERNAME_AND_EMAIL`', '`USERNAME_AND_OPTIONAL_EMAIL`', '`USERNAME_ONLY`', or '`EMAIL_ONLY`' (default)." - } - ] -}; - -Template.api.accounts_validateNewUser = { - id: "accounts_validatenewuser", - name: "Accounts.validateNewUser(func)", - locus: "Server", - descr: ["Set restrictions on new user creation."], - args: [ - { - name: "func", - type: "Function", - descr: "Called whenever a new user is created. Takes the new user object, and returns true to allow the creation or false to abort." - } - ] -}; - -Template.api.accounts_onCreateUser = { - id: "accounts_oncreateuser", - name: "Accounts.onCreateUser(func)", - locus: "Server", - descr: ["Customize new user creation."], - args: [ - { - name: "func", - type: "Function", - descr: "Called whenever a new user is created. Return the new user object, or throw an `Error` to abort the creation." - } - ] -}; - - - - Template.api.setTimeout = { id: "meteor_settimeout", name: "Meteor.setTimeout", @@ -1402,8 +1403,7 @@ Template.api.template_data = { }; var rfc = function (descr) { - return ('RFC5322' - + ' ' + descr); + return '[RFC5322](http://tools.ietf.org/html/rfc5322) ' + descr; }; Template.api.email_send = { diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 06d4bca77f..864a4025ac 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -22,12 +22,12 @@ when writing those apps.

Structuring your application

-A Meteor application is a mix of JavaScript that runs inside a -client web browser, JavaScript that runs on the Meteor server inside -a Node.js container, and all the supporting HTML fragments, CSS rules, -and static assets. Meteor automates the packaging and transmission -of these different components. And, it is quite flexible about how -you choose to structure those components in your file tree. +A Meteor application is a mix of JavaScript that runs inside a client web +browser, JavaScript that runs on the Meteor server inside a +[Node.js](http://nodejs.org/) container, and all the supporting HTML fragments, +CSS rules, and static assets. Meteor automates the packaging and transmission +of these different components. And, it is quite flexible about how you choose +to structure those components in your file tree. The only server asset is JavaScript. Meteor gathers all your JavaScript files, excluding anything under the `client` @@ -108,8 +108,7 @@ your client model code. Meteor's protocol for distributing document updates is database agnostic. By default, Meteor applications use the -familiar MongoDB API: +familiar [MongoDB API](http://www.mongodb.org/display/DOCS/Manual): servers store documents in MongoDB collections, and clients cache those documents in a client-side cache that implements the same Mongo API for queries and updates. @@ -149,14 +148,8 @@ server-side database driver and/or a client-side cache that implements an alternative API. The `mongo-livedata` is a good starting point for such a project. -{{#note}} -A pre-release version of Meteor includes a user login system and a set of tools -for securing read and write access to data based on the logged-in user. For more -information, see the -Getting -Started with Auth wiki page. -{{/note}} +XXX should we mention security/auth at all here, or just expect folks to keep +reading until the accounts section? {{/better_markdown}} @@ -166,11 +159,10 @@ Started with Auth wiki page.

Reactivity

-Meteor embraces the concept of - -reactive programming. This means that you can write your code in a -simple imperative style, and the result will be automatically -recalculated whenever data changes that your code depends on. +Meteor embraces the concept of [reactive +programming](http://en.wikipedia.org/wiki/Reactive_programming). This means that +you can write your code in a simple imperative style, and the result will be +automatically recalculated whenever data changes that your code depends on. Meteor.autosubscribe(function () { Meteor.subscribe("messages", Session.get("currentRoomId")); @@ -214,10 +206,11 @@ And the reactive data sources that can trigger changes are: * [`Meteor.userId`](#meteor_userid) * [`Meteor.userLoaded`](#meteor_userloaded) -Meteor's implementation -of reactivity is short and sweet, about 50 lines of code. You can -hook into it yourself to add new reactive contexts or data sources, -using the [`Meteor.deps`](#meteor_deps) module. +Meteor's +[implementation](https://github.com/meteor/meteor/blob/master/packages/deps/deps.js) +of reactivity is short and sweet, about 50 lines of code. You can hook into it +yourself to add new reactive contexts or data sources, using the +[`Meteor.deps`](#meteor_deps) module. {{/better_markdown}} @@ -267,15 +260,14 @@ the new elements using a library like jQuery. In that case, call [`Meteor.flush`](#meteor_flush) to bring the DOM up to date immediately. -When live-updating DOM elements are taken off the screen, they are -automatically cleaned up — their callbacks are torn down, any -associated database queries are stopped, and they stop updating. For -this reason, you never have to worry about -the zombie templates that plague hand-written update -logic. To protect your elements from cleanup, just make sure that they -on-screen before your code returns to the event loop, or before any -call you make to [`Meteor.flush`](#meteor_flush). +When live-updating DOM elements are taken off the screen, they are automatically +cleaned up — their callbacks are torn down, any associated database +queries are stopped, and they stop updating. For this reason, you never have to +worry about the [zombie +templates](http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/) +that plague hand-written update logic. To protect your elements from cleanup, +just make sure that they on-screen before your code returns to the event loop, +or before any call you make to [`Meteor.flush`](#meteor_flush). Another thorny problem in hand-written applications is element preservation. Suppose the user is typing text into an `` @@ -313,10 +305,9 @@ available as a function on the global `Template` object. {{#note}} Today, the only templating system that has been packaged for Meteor is Handlebars. Let us know what templating systems you'd like to use with -Meteor. Meanwhile, see -the Handlebars documentation -and Meteor -Handlebars extensions. +Meteor. Meanwhile, see the [Handlebars +documentation](http://www.handlebarsjs.com/) and [Meteor Handlebars +extensions](https://github.com/meteor/meteor/wiki/Handlebars). {{/note}} A template with a `name` of `hello` is rendered by calling the diff --git a/docs/client/docs.css b/docs/client/docs.css index b4661d1730..4f98beac79 100644 --- a/docs/client/docs.css +++ b/docs/client/docs.css @@ -436,11 +436,11 @@ pre { @media (min-width: 1024px) { /* ipad landscape and desktop */ #main { - width: 600px; - margin-left: 310px; /* nav width + padding */ + width: 610px; + margin-left: 330px; /* nav width + padding */ } #nav { - width: 250px; + width: 270px; } .github-ribbon { display: block; diff --git a/docs/client/docs.js b/docs/client/docs.js index d9612497e0..5190ee9ad1 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -48,18 +48,29 @@ Meteor.startup(function () { } }); + window.onhashchange = function () { + scrollToSection(location.hash); + }; + + var scrollToSection = function (section) { + ignore_waypoints = true; + Session.set("section", section.substr(1)); + scroller().animate({ + scrollTop: $(section).offset().top + }, 500, 'swing', function () { + window.location.hash = section; + ignore_waypoints = false; + }); + }; + $('#main, #nav').delegate("a[href^='#']", 'click', function (evt) { evt.preventDefault(); var sel = $(this).attr('href'); - ignore_waypoints = true; - Session.set("section", sel.substr(1)); - scroller().animate({ - scrollTop: $(sel).offset().top - }, 500, 'swing', function () { - window.location.hash = sel; - ignore_waypoints = false; - }); + scrollToSection(sel); }); + + // Make external links open in a new tab. + $('a:not([href^="#"])').attr('target', '_blank'); }); var toc = [ @@ -89,13 +100,13 @@ var toc = [ "Publish and subscribe", [ "Meteor.publish", [ + {instance: "this", name: "userId", id: "publish_userId"}, {instance: "this", name: "set", id: "publish_set"}, {instance: "this", name: "unset", id: "publish_unset"}, {instance: "this", name: "complete", id: "publish_complete"}, {instance: "this", name: "flush", id: "publish_flush"}, {instance: "this", name: "onStop", id: "publish_onstop"}, - {instance: "this", name: "stop", id: "publish_stop"}, - {instance: "this", name: "userId", id: "publish_userId"} + {instance: "this", name: "stop", id: "publish_stop"} ], "Meteor.subscribe", "Meteor.autosubscribe" @@ -141,7 +152,8 @@ var toc = [ {type: "spacer"}, {name: "Selectors", style: "noncode"}, {name: "Modifiers", style: "noncode"}, - {name: "Sort specifiers", style: "noncode"} + {name: "Sort specifiers", style: "noncode"}, + {name: "Field specifiers", style: "noncode"} ], "Session", [ @@ -150,6 +162,41 @@ var toc = [ "Session.equals" ], + {name: "Accounts", id: "accounts_api"}, [ + "Meteor.user", + "Meteor.userId", + "Meteor.users", + "Meteor.userLoaded", + "Meteor.logout", + "Meteor.loginWithPassword", + {name: "Meteor.loginWithFacebook", id: "meteor_loginwithexternalservice"}, + {name: "Meteor.loginWithGithub", id: "meteor_loginwithexternalservice"}, + {name: "Meteor.loginWithGoogle", id: "meteor_loginwithexternalservice"}, + {name: "Meteor.loginWithTwitter", id: "meteor_loginwithexternalservice"}, + {name: "Meteor.loginWithWeibo", id: "meteor_loginwithexternalservice"}, + {type: "spacer"}, + + "Accounts.config", + "Accounts.ui.config", + "Accounts.validateNewUser", + "Accounts.onCreateUser" + ], + + {name: "Passwords", id: "accounts_passwords"}, [ + "Accounts.createUser", + "Accounts.changePassword", + "Accounts.forgotPassword", + "Accounts.resetPassword", + "Accounts.setPassword", + "Accounts.verifyEmail", + {type: "spacer"}, + + "Accounts.sendResetPasswordEmail", + "Accounts.sendEnrollmentEmail", + "Accounts.sendVerificationEmail", + "Accounts.emailTemplates" + ], + {name: "Templates", id: "templates_api"}, [ {prefix: "Template", instance: "myTemplate", id: "template_call"}, [ {name: "rendered", id: "template_rendered"}, @@ -174,41 +221,6 @@ var toc = [ {name: "Reactivity isolation", style: "noncode", id: "isolate"} ], - {name: "Accounts", id: "accounts_api"}, [ - "Meteor.user", - "Meteor.userId", - "Meteor.users", - "Meteor.userLoaded", - "Meteor.logout", - "Meteor.loginWithPassword", - {name: "Meteor.loginWithFacebook", id: "meteor_loginwithoauth"}, - {name: "Meteor.loginWithGithub", id: "meteor_loginwithoauth"}, - {name: "Meteor.loginWithGoogle", id: "meteor_loginwithoauth"}, - {name: "Meteor.loginWithTwitter", id: "meteor_loginwithoauth"}, - {name: "Meteor.loginWithWeibo", id: "meteor_loginwithoauth"}, - {type: "spacer"}, - - "Accounts.createUser", - "Accounts.changePassword", - "Accounts.forgotPassword", - "Accounts.resetPassword", - "Accounts.setPassword", - "Accounts.verifyEmail", - {type: "spacer"}, - - "Accounts.sendResetPasswordEmail", - "Accounts.sendEnrollmentEmail", - "Accounts.sendVerificationEmail", - "Accounts.emailTemplates", - {type: "spacer"}, - - "Accounts.config", - "Accounts.ui.config", - "Accounts.validateNewUser", - "Accounts.onCreateUser" - ], - - "Timers", [ "Meteor.setTimeout", "Meteor.setInterval", diff --git a/docs/client/introduction.html b/docs/client/introduction.html index a097e2be23..ce4bc72bc5 100644 --- a/docs/client/introduction.html +++ b/docs/client/introduction.html @@ -39,7 +39,8 @@ our thinking. We'd love to hear your feedback. -The following works on all supported platforms. +The following works on all [supported +platforms](https://github.com/meteor/meteor/wiki/Supported-Platforms). Install Meteor: @@ -106,25 +107,24 @@ clean, classically beautiful APIs.

Developer Resources

-Fork me on GitHub +Fork me on GitHub If anything in Meteor catches your interest, we hope you'll get involved with the project!
Stack Overflow
-
The best place to ask (and answer!) technical questions is - on Stack - Overflow. Be sure to add the meteor tag to your - question. +
The best place to ask (and answer!) technical questions is on [Stack + Overflow](http://stackoverflow.com/questions/tagged/meteor). Be sure to add + the meteor tag to your question.
Mailing lists
- We have two mailing lists for Meteor. meteor-talk@googlegroups.com + We have two mailing lists for Meteor. meteor-talk@googlegroups.com is for general questions, requests for help, and new project announcements. - meteor-core@googlegroups.com + meteor-core@googlegroups.com is for discussing Meteor internals and proposed changes.
@@ -134,7 +134,7 @@ developers hang out here and will answer your questions whenever they can.
GitHub
-
The code is on GitHub. The best way to send a patch is with a GitHub pull request, and the best way to file a bug is in the GitHub bug tracker.
+
The code is on GitHub. The best way to send a patch is with a GitHub pull request, and the best way to file a bug is in the GitHub bug tracker.
{{/markdown}} diff --git a/docs/client/packages/accounts-ui.html b/docs/client/packages/accounts-ui.html index 7e7c4f0130..2c42ccfab7 100644 --- a/docs/client/packages/accounts-ui.html +++ b/docs/client/packages/accounts-ui.html @@ -5,21 +5,25 @@ A turn-key user interface for Meteor Accounts. To add Accounts and a set of login controls to an application add the -`accounts-ui` package and one or more login provider package: +`accounts-ui` package and at least one login provider package: `accounts-password`, `accounts-facebook`, `accounts-github`, -`accounts-google`, `accounts-twitter`, and `accounts-weibo`. +`accounts-google`, `accounts-twitter`, or `accounts-weibo`. -Then simply add the `{{dstache}}loginButtons}}` helper to an HTML -file. This will place a login widget on the page. If there is only one -OAuth provider configured, this will add one login/logout button. If you -use `accounts-password` or have more than one OAuth provider, this will -add a 'Sign in' link which opens a dropdown menu with login options. To -make the login dropdown right aligned, use `{{dstache}}loginButtons align=right}}`. +Then simply add the `{{dstache}}loginButtons}}` helper to an HTML file. This +will place a login widget on the page. If there is only one provider configured +and it is an external service, this will add a login/logout button. If you use +`accounts-password` or use multiple external login services, this will add +a "Sign in" link which opens a dropdown menu with login options. To make the +login dropdown right aligned (useful if you position the login buttons +at the right edge of the screen), use `{{dstache}}loginButtons align=right}}`. + +To configure the behavior of `{{dstache}}loginButtons}}`, use +[`Accounts.ui.config`](#accounts_ui_config). `accounts-ui` also includes modal popup dialogs to handle links from [`sendResetPasswordEmail`](#accounts_sendresetpasswordemail), [`sendVerificationEmail`](#accounts_sendverificationemail), and [`sendEnrollmentEmail`](#accounts_sendenrollmentemail). These -do not have be manually placed in HTML, they are automatically activated +do not have be manually placed in HTML: they are automatically activated when the URLs are loaded. diff --git a/docs/client/packages/amplify.html b/docs/client/packages/amplify.html index 73a6d24ba3..1afe28df47 100644 --- a/docs/client/packages/amplify.html +++ b/docs/client/packages/amplify.html @@ -10,6 +10,7 @@ components, and several useful utility functions. Amplify defines a global namespace `amplify` on the client only. It does not run on the server. -For more information about Amplify, see http://amplifyjs.com/. +For more information about Amplify, see . + {{/better_markdown}} diff --git a/docs/client/packages/backbone.html b/docs/client/packages/backbone.html index 30675412c6..f12fc61b12 100644 --- a/docs/client/packages/backbone.html +++ b/docs/client/packages/backbone.html @@ -8,7 +8,7 @@ functionality, it also provides an API for HTML5 pushState and client-side URL routing. For more information about Backbone, see -http://documentcloud.github.com/backbone/. +. {{/better_markdown}} diff --git a/docs/client/packages/bootstrap.html b/docs/client/packages/bootstrap.html index aa2f4ccdfe..62a712298f 100644 --- a/docs/client/packages/bootstrap.html +++ b/docs/client/packages/bootstrap.html @@ -9,7 +9,7 @@ interactions including typography, forms, buttons, tables, grids, and navigation. For more information about Bootstrap, see -http://twitter.github.com/bootstrap/. +. {{/better_markdown}} diff --git a/docs/client/packages/coffeescript.html b/docs/client/packages/coffeescript.html index 6cd1cfc165..3f24ce8c54 100644 --- a/docs/client/packages/coffeescript.html +++ b/docs/client/packages/coffeescript.html @@ -10,8 +10,7 @@ interpretation at runtime. CoffeeScript is supported on both the client and the server. Files ending with `.coffee` are automatically compiled to JavaScript. -See http://jashkenas.github.com/coffee-script/ -for more information. +See for more information. {{/better_markdown}} diff --git a/docs/client/packages/d3.html b/docs/client/packages/d3.html index a7cf49a82b..f361fa27c8 100644 --- a/docs/client/packages/d3.html +++ b/docs/client/packages/d3.html @@ -3,7 +3,7 @@ ## `d3` -[D3.js](http://d3js.org/D3.js) is a JavaScript library for manipulating +[D3.js](http://d3js.org/) is a JavaScript library for manipulating documents based on data. D3 helps you bring data to life using HTML, SVG and CSS. D3's emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, diff --git a/docs/client/packages/jquery.html b/docs/client/packages/jquery.html index f102668754..989160cd2d 100644 --- a/docs/client/packages/jquery.html +++ b/docs/client/packages/jquery.html @@ -3,7 +3,7 @@ ## `jquery` -jQuery is a fast and concise JavaScript +[jQuery](http://jquery.com/) is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. diff --git a/docs/client/packages/less.html b/docs/client/packages/less.html index 731eeb0fc2..da85fe95bb 100644 --- a/docs/client/packages/less.html +++ b/docs/client/packages/less.html @@ -10,8 +10,7 @@ With the `less` package installed, `.less` files in your application are automatically compiled to CSS and the results are included in the client CSS bundle. -See http://lesscss.org/ for -documentation of the LESS language. +See for documentation of the LESS language. {{/better_markdown}} diff --git a/docs/client/packages/sass.html b/docs/client/packages/sass.html index 7db05566a7..51c917ecf4 100644 --- a/docs/client/packages/sass.html +++ b/docs/client/packages/sass.html @@ -7,10 +7,15 @@ expresions. With the `sass` package installed, `.sass` files in your application are automatically compiled to CSS and the results are included in the client CSS bundle. -See https://github.com/visionmedia/sass.js -for the JavaScript implementation of the Sass language -and http://sass-lang.com/ for the -original project. +See for the JavaScript implementation +of the Sass language and for the original project. + +{{#warning}} +The Sass JavaScript implementation used by Node is unmaintained and doesn't +implement the newest language syntax documented at . It +may be removed from a future version of Meteor; consider using [Less](#less) or +[Stylus](#stylus) instead. +{{/warning}} {{/better_markdown}} diff --git a/docs/client/packages/spiderable.html b/docs/client/packages/spiderable.html index 08c9d619bb..3bd785c4d4 100644 --- a/docs/client/packages/spiderable.html +++ b/docs/client/packages/spiderable.html @@ -3,37 +3,33 @@ ## `spiderable` -The `spiderable` package is a temporary solution to allow web search -engines to index a Meteor application. It uses the AJAX -Crawling specification published by Google to serve HTML to -compatible spiders (Google, Bing, Yandex, and more). +The `spiderable` package is a temporary solution to allow web search engines to +index a Meteor application. It uses the [AJAX Crawling +specification](https://developers.google.com/webmasters/ajax-crawling/) +published by Google to serve HTML to compatible spiders (Google, Bing, Yandex, +and more). -When a spider requests an HTML snapshot of a page the Meteor server runs -the client half of the application inside phantomjs, a headless browser, and -returns the full HTML generated by the client code. +When a spider requests an HTML snapshot of a page the Meteor server runs the +client half of the application inside [phantomjs](http://phantomjs.org/), a +headless browser, and returns the full HTML generated by the client code. {{#warning}} This is a temporary approach to allow Meteor applications to be searchable. Expect significant changes to this package. {{/warning}} -In order to have links between multiple pages on a site visible to -spiders, apps must use real links (eg ``) rather than -simply re-rendering portions of the page when an element is -clicked. Apps should render their content based on the URL of the page -and can use -HTML5 pushState -to alter the URL on the client without triggering a page reload. See the -Todos example -for a demonstration. +In order to have links between multiple pages on a site visible to spiders, apps +must use real links (eg ``) rather than simply re-rendering +portions of the page when an element is clicked. Apps should render their +content based on the URL of the page and can use [HTML5 +pushState](https://developer.mozilla.org/en-US/docs/DOM/Manipulating_the_browser_history) +to alter the URL on the client without triggering a page reload. See the [Todos +example](http://meteor.com/examples/todos) for a demonstration. {{#warning}} If you deploy your application with `meteor bundle`, you must install -`phantomjs` (http://phantomjs.org) somewhere in your +`phantomjs` ([http://phantomjs.org](http://phantomjs.org/)) somewhere in your `$PATH`. If you use `meteor deploy` this is already taken care of. {{/warning}} diff --git a/docs/client/packages/stylus.html b/docs/client/packages/stylus.html index 0e683b19f6..4665873c37 100644 --- a/docs/client/packages/stylus.html +++ b/docs/client/packages/stylus.html @@ -14,10 +14,9 @@ The `stylus` package also includes `nib` support. Add `@import 'nib'` to your `.styl` files to enable cross-browser mixins such as `linear-gradient` and `border-radius`. -See http://learnboost.github.com/stylus -for documentation of the Stylus language, -and http://visionmedia.github.com/nib -for documentation of the nib extensions. +See for documentation of the Stylus +language, and for documentation of the nib +extensions. {{/better_markdown}} diff --git a/docs/client/packages/underscore.html b/docs/client/packages/underscore.html index 1b570d5a11..ae7f2ad88b 100644 --- a/docs/client/packages/underscore.html +++ b/docs/client/packages/underscore.html @@ -9,8 +9,8 @@ concise JavaScript in a functional style. The `underscore` package defines the `_` namespace on both the client and the server. -See http://documentcloud.github.com/underscore/ -for underscore API documentation. +See for underscore API +documentation. {{#warning}} Currently, underscore is included in all projects, as the Meteor diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index f83aced014..adeb721346 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -12,12 +12,22 @@ if (!Accounts._options) { // - forbidClientAccountCreation {Boolean} // Do not allow clients to create accounts directly. Accounts.config = function(options) { - _.each(["sendVerificationEmail", "forbidClientAccountCreation"], function(key) { + // validate option keys + var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation"]; + _.each(_.keys(options), function (key) { + if (!_.contains(VALID_KEYS, key)) { + throw new Error("Accounts.config: Invalid key: " + key); + } + }); + + // set values in Accounts._options + _.each(VALID_KEYS, function (key) { if (key in options) { - if (key in Accounts._options) + if (key in Accounts._options) { throw new Error("Can't set `" + key + "` more than once"); - else + } else { Accounts._options[key] = options[key]; + } } }); }; diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 9e681fbc88..c75ca1e87b 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -316,7 +316,7 @@ return true; }, - fields: ['_id'] // we only look at _id. + fetch: ['_id'] // we only look at _id. }); /// DEFAULT INDEXES ON USERS diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index b771e17de1..a60b8c0890 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -1,3 +1,12 @@ +// XXX it'd be cool to also test that the right thing happens if options +// *are* validated, but Accounts._options is global state which makes this hard +// (impossible?) +Tinytest.add('accounts - config validates keys', function (test) { + test.throws(function () { + Accounts.config({foo: "bar"}); + }); +}); + Tinytest.add('accounts - updateOrCreateUserFromExternalService', function (test) { var facebookId = Meteor.uuid(); var weiboId1 = Meteor.uuid(); diff --git a/packages/accounts-github/github_client.js b/packages/accounts-github/github_client.js index 047123e3a3..c16db9680d 100644 --- a/packages/accounts-github/github_client.js +++ b/packages/accounts-github/github_client.js @@ -13,14 +13,13 @@ } var state = Meteor.uuid(); - var required_scope = ['user']; - var scope = _.union((options && options.requestPermissions) || [], required_scope); - var flat_scope = _.map(scope, encodeURIComponent).join('+'); + var scope = (options && options.requestPermissions) || []; + var flatScope = _.map(scope, encodeURIComponent).join('+'); var loginUrl = 'https://github.com/login/oauth/authorize' + '?client_id=' + config.clientId + - '&scope=' + flat_scope + + '&scope=' + flatScope + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/github?close') + '&state=' + state; diff --git a/packages/accounts-password/email_tests.js b/packages/accounts-password/email_tests.js index c119bf0030..4691266c47 100644 --- a/packages/accounts-password/email_tests.js +++ b/packages/accounts-password/email_tests.js @@ -107,8 +107,8 @@ test.equal(Meteor.user().emails.length, 1); test.equal(Meteor.user().emails[0].address, email2); test.isFalse(Meteor.user().emails[0].verified); - // We should NOT be publishing verification tokens! - test.isFalse(_.has(Meteor.user(), 'emailVerificationTokens')); + // We should NOT be publishing things like verification tokens! + test.isFalse(_.has(Meteor.user(), 'services')); }, function (test, expect) { getVerifyEmailToken(email2, test, expect); diff --git a/packages/accounts-password/passwords_server.js b/packages/accounts-password/passwords_server.js index 6f0972efad..5d907de04d 100644 --- a/packages/accounts-password/passwords_server.js +++ b/packages/accounts-password/passwords_server.js @@ -138,13 +138,15 @@ if (!token) throw new Meteor.Error(400, "Need to pass token"); - var user = Meteor.users.findOne({'emailVerificationTokens.token': token}); + var user = Meteor.users.findOne( + {'services.email.verificationTokens.token': token}); if (!user) throw new Meteor.Error(403, "Verify email link expired"); - var tokenRecord = _.find(user.emailVerificationTokens, function (t) { - return t.token == token; - }); + var tokenRecord = _.find(user.services.email.verificationTokens, + function (t) { + return t.token == token; + }); if (!tokenRecord) throw new Meteor.Error(403, "Verify email link expired"); @@ -166,7 +168,7 @@ {_id: user._id, 'emails.address': tokenRecord.address}, {$set: {'emails.$.verified': true}, - $pull: {emailVerificationTokens: {token: token}}, + $pull: {'services.email.verificationTokens': {token: token}}, $push: {'services.resume.loginTokens': stampedLoginToken}}); this.setUserId(user._id); @@ -234,8 +236,9 @@ token: Meteor.uuid(), address: address, when: +(new Date)}; - Meteor.users.update({_id: userId}, - {$push: {emailVerificationTokens: tokenRecord}}); + Meteor.users.update( + {_id: userId}, + {$push: {'services.email.verificationTokens': tokenRecord}}); var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token); Email.send({ @@ -442,7 +445,7 @@ // XXX allow an optional callback? if (callback) { - throw new Error("Meteor.createUser with callback not supported on the server yet."); + throw new Error("Accounts.createUser with callback not supported on the server yet."); } var userId = createUser(options).id; diff --git a/packages/accounts-ui-unstyled/accounts_ui.js b/packages/accounts-ui-unstyled/accounts_ui.js index 46c823b9b0..770a77c9de 100644 --- a/packages/accounts-ui-unstyled/accounts_ui.js +++ b/packages/accounts-ui-unstyled/accounts_ui.js @@ -9,6 +9,14 @@ if (!Accounts.ui._options) { Accounts.ui.config = function(options) { + // validate options keys + var VALID_KEYS = ['passwordSignupFields', 'requestPermissions']; + _.each(_.keys(options), function (key) { + if (!_.contains(VALID_KEYS, key)) + throw new Error("Accounts.ui.config: Invalid key: " + key); + }); + + // deal with `passwordSignupFields` if (options.passwordSignupFields) { if (_.contains([ "USERNAME_AND_EMAIL", @@ -17,20 +25,24 @@ Accounts.ui.config = function(options) { "EMAIL_ONLY" ], options.passwordSignupFields)) { if (Accounts.ui._options.passwordSignupFields) - throw new Error("Can't set `passwordSignupFields` more than once"); + throw new Error("Accounts.ui.config: Can't set `passwordSignupFields` more than once"); else Accounts.ui._options.passwordSignupFields = options.passwordSignupFields; } else { - throw new Error("Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); + throw new Error("Accounts.ui.config: Invalid option for `passwordSignupFields`: " + options.passwordSignupFields); } } + // deal with `requestPermissions` if (options.requestPermissions) { _.each(options.requestPermissions, function (scope, service) { - if (Accounts.ui._options.requestPermissions[service]) - throw new Error("Can't set `requestPermissions` more than once for " + service); - else + if (Accounts.ui._options.requestPermissions[service]) { + throw new Error("Accounts.ui.config: Can't set `requestPermissions` more than once for " + service); + } else if (!(scope instanceof Array)) { + throw new Error("Accounts.ui.config: Value for `requestPermissions` must be an array"); + } else { Accounts.ui._options.requestPermissions[service] = scope; + } }); } }; diff --git a/packages/accounts-ui-unstyled/accounts_ui_tests.js b/packages/accounts-ui-unstyled/accounts_ui_tests.js new file mode 100644 index 0000000000..e65989f5d8 --- /dev/null +++ b/packages/accounts-ui-unstyled/accounts_ui_tests.js @@ -0,0 +1,17 @@ +// XXX it'd be cool to also test that the right thing happens if options +// *are* validated, but Accouns.ui._options is global state which makes this hard +// (impossible?) +Tinytest.add('accounts-ui - config validates keys', function (test) { + test.throws(function () { + Accounts.ui.config({foo: "bar"}); + }); + + test.throws(function () { + Accounts.ui.config({passwordSignupFields: "not a valid option"}); + }); + + test.throws(function () { + Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); + }); +}); + diff --git a/packages/accounts-ui-unstyled/login_buttons_dropdown.js b/packages/accounts-ui-unstyled/login_buttons_dropdown.js index 2d4e550891..121c4aaeea 100644 --- a/packages/accounts-ui-unstyled/login_buttons_dropdown.js +++ b/packages/accounts-ui-unstyled/login_buttons_dropdown.js @@ -385,7 +385,7 @@ var signup = function () { loginButtonsSession.resetMessages(); - var options = {}; // to be passed to Meteor.createUser + var options = {}; // to be passed to Accounts.createUser var username = trimmedElementValueById('login-username'); if (username !== null) { diff --git a/packages/accounts-ui-unstyled/package.js b/packages/accounts-ui-unstyled/package.js index 4defc581ad..ef41ac3bf0 100644 --- a/packages/accounts-ui-unstyled/package.js +++ b/packages/accounts-ui-unstyled/package.js @@ -21,3 +21,9 @@ Package.on_use(function (api) { 'login_buttons_dropdown.js', 'login_buttons_dialogs.js'], 'client'); }); + +Package.on_test(function (api) { + api.use('accounts-ui-unstyled'); + api.use('tinytest'); + api.add_files('accounts_ui_tests.js', 'client'); +}); diff --git a/packages/mongo-livedata/allow_tests.js b/packages/mongo-livedata/allow_tests.js index 15f92cde81..84a6c44e39 100644 --- a/packages/mongo-livedata/allow_tests.js +++ b/packages/mongo-livedata/allow_tests.js @@ -186,6 +186,43 @@ // if (Meteor.isServer) { + Tinytest.add("collection - allow and deny validate options", function (test) { + var collection = new Meteor.Collection(null); + + test.throws(function () { + collection.allow({invalidOption: true}); + }); + test.throws(function () { + collection.deny({invalidOption: true}); + }); + + _.each(['insert', 'update', 'remove', 'fetch'], function (key) { + var options = {}; + options[key] = true; + test.throws(function () { + collection.allow(options); + }); + test.throws(function () { + collection.deny(options); + }); + }); + + _.each(['insert', 'update', 'remove'], function (key) { + var options = {}; + options[key] = ['an array']; // this should be a function, not an array + test.throws(function () { + collection.allow(options); + }); + test.throws(function () { + collection.deny(options); + }); + }); + + test.throws(function () { + collection.allow({fetch: function () {}}); // this should be an array + }); + }); + Tinytest.add("collection - calling allow restricts", function (test) { var collection = new Meteor.Collection(null); test.equal(collection._restricted, false); diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 7ebfdc8136..4bb3ec4bc7 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -184,7 +184,7 @@ _.each(["insert", "update", "remove"], function (name) { // down. callback = function (err) { if (err) - Meteor._debug(name + " failed: " + err.error + " -- " + err.reason); + Meteor._debug(name + " failed: " + (err.reason || err.stack)); }; } @@ -281,19 +281,34 @@ Meteor.Collection.prototype._ensureIndex = function (index, options) { (function () { var addValidator = function(allowOrDeny, options) { + // validate keys + var VALID_KEYS = ['insert', 'update', 'remove', 'fetch']; + _.each(_.keys(options), function (key) { + if (!_.contains(VALID_KEYS, key)) + throw new Error(allowOrDeny + ": Invalid key: " + key); + }); + var self = this; self._restricted = true; _.each(['insert', 'update', 'remove'], function (name) { - if (options[name]) + if (options[name]) { + if (!(options[name] instanceof Function)) { + throw new Error(allowOrDeny + ": Value for `" + name + "` must be a function"); + } self._validators[name][allowOrDeny].push(options[name]); + } }); // Only update the fetch fields if we're passed things that affect // fetching. This way allow({}) and allow({insert: f}) don't result in // setting fetchAllFields - if (options.update || options.remove || options.fetch) + if (options.update || options.remove || options.fetch) { + if (options.fetch && !(options.fetch instanceof Array)) { + throw new Error(allowOrDeny + ": Value for `fetch` must be an array"); + } self._updateFetch(options.fetch); + } }; Meteor.Collection.prototype.allow = function(options) {