merge commit

This commit is contained in:
Aaron Fay
2012-09-20 20:40:43 -07:00
49 changed files with 1869 additions and 244 deletions

View File

@@ -1,8 +1,9 @@
language: node_js
services: mongodb
node_js:
- 0.6
- 0.8
branches:
only:
- master
- master

View File

@@ -1,20 +1,28 @@
# History
## 0.6.5
## 0.6.4
- Fixed incorrect Content-Length response header.
## 0.6.3
- Removed dependency on jQuery for dpd.js
- JSON-formatted "bad credentials" login error
- Improved error reporting on CLI when port is in use
- If in development mode, and no port has been specifically requested, CLI will retry with up to 5 different ports- Fixed "no open connections" bug on startup
- If in development mode, and no port has been specifically requested, CLI will retry with up to 5 different ports
- Fixed "no open connections" bug on startup
- Renamed `Db.connect()` to `Db.create()`
- Db connections are now lazy and only occur once a request is made
- Added 500 and 404 error pages
- Added module domain error handling for better module errors
- Added automatic reloading on error
- Dropped support for node 0.6
## 0.6.2
- Fixed rare but annoying bug where server would crash for no good reason ("Cannot set headers") on a request
- Fixed rare but annoying bug where server would crash for no good reason ("Cannot set headers") on a request
## 0.6.1

View File

@@ -1,10 +1,10 @@
# deployd v0.6.2
# deployd v0.6.4
[![Build Status](https://secure.travis-ci.org/deployd/deployd.png)](http://travis-ci.org/deployd/deployd)
## overview
Deployd is a toolkit for building realtime APIs for web and mobile apps. Ready-made, configurable Resources add common functionality to a Deployd backend, which can be further customized with JavaScript Events.
Deployd is the simplest way to build realtime APIs for web and mobile apps. Ready-made, configurable Resources add common functionality to a Deployd backend, which can be further customized with JavaScript Events.
[Read more about deployd](http://deployd.com)
@@ -30,6 +30,10 @@ Deployd is a toolkit for building realtime APIs for web and mobile apps. Ready-m
- [Community Discussion Page](http://deployd.com/community.html)
- [Example Apps](http://deployd.com/docs/examples.html)
## install from npm
npm install deployd -g
## install from source
git clone https://github.com/deployd/deployd.git
@@ -38,9 +42,9 @@ Deployd is a toolkit for building realtime APIs for web and mobile apps. Ready-m
## unit tests
$ cd deployd
$ mongod &
$ mocha
cd deployd
mongod &
npm test
## integration tests

View File

@@ -5,7 +5,7 @@
*/
var program = require('commander')
, deployd = require('../')
, deployd = require('../').createMonitor
, repl = require('../lib/client/repl')
, shelljs = require('shelljs/global')
, mongod = require('../lib/util/mongod')

11
docs/about.md Normal file
View File

@@ -0,0 +1,11 @@
# About Deployd Server
We call Deployd a **resource server**. A resource server is not a library, but a complete server that works out of the box, and can be customized to fit the needs of your app by adding resources. Resources are ready-made components that live at a URL and provide functionality to your client app.
An example of a resource is a data collection. You only have to define the properties and types of objects, and the server will validate the data. You will also be able to create your own custom resources and install custom resources from other developers. (we're built on Node.js, so custom resources will take the form of node modules).
## Install
- [Download](http://www.deployd.com/download.html) the OSX installer (13.8mb).
- [Download](http://www.deployd.com/download.html) the Windows installer (13.8mb).

23
docs/deploy.md Normal file
View File

@@ -0,0 +1,23 @@
# Deploying Your App
When you want to share your app with the world, you can use Deployd's beta hosting service to host it online in seconds.
*Note: this service is heavily in development and will change drastically in the future*
In your Deployd app folder, type the command:
dpd deploy [subdomain]
If you do not provide a subdomain, it will automatically use the app's folder name.
*Note: if you recieve a "not allowed" error, it means that the subdomain you requested is in use by another app and you don't have the credentials to push to it. In that case, you choose another subdomain.*
When it is done, you can access your app at `[subdomain].deploydapp.com`.
# Accessing Your App's Dashboard
To access your app's dashboard (for example, to add data), you can go to `[subdomain].deploydapp.com/dashboard` or type `dpd remote`. The Dashboard will prompt you for a key, type `dpd showkey` to print this key to the console and paste it into the box.
# Working with collaborators
To provide additional collaborators access to push new versions and access the dashboard, you can copy the `deployments.json` and `keys.json` files out of your app's `.dpd` directory and give them to your collaborators. Your collaborators can then paste these files in their own `.dpd` directory and use the `deploy`, `remote`, and `showkey` commands.

32
docs/examples.md Normal file
View File

@@ -0,0 +1,32 @@
# Example Apps
These demo apps are just a taste of what you can do with Deployd. Browse their source code to see how they work.
## Poll Demo
![Poll Demo](http://deployd.com/img/examples/poll.png)
A simple survey app. You can vote in it and change your response, and other users will see the bar graph update immediately. Download the source to see how to aggregate data in real time.
<a class="btn btn-primary" target="_blank" href="http://poll2.deploydapp.com"><i class="icon-white icon-eye-open"></i> View Online</a>
<a class="btn btn-primary" target="_blank" href="/downloads/examples/poll.zip"><i class="icon-white icon-download"></i> Download Source</a>
## Users Demo
![Users Demo](http://deployd.com/img/examples/users.png)
A simple messaging app. Register, login in, and post messages for other users to see. Download the source to see how to simply add user authentication to your app.
<a class="btn btn-primary" target="_blank" href="http://users.deploydapp.com/login.html"><i class="icon-white icon-eye-open"></i> View Online</a>
<a class="btn btn-primary" target="_blank" href="/downloads/examples/users.zip"><i class="icon-white icon-download"></i> Download Source</a>
## Trivia Demo
![Trivia Demo](http://deployd.com/img/examples/trivia.png)
A multiple-choice trivia game. See how much you know about JavaScript, and how much everybody else knows! Download the source to see how to add secure gamification to your app.
*Note: There's only three questions right now! Help us out by posting your favorite JavaScript quirks on the [community page](http://deployd.com/community.html)*
<a class="btn btn-primary" target="_blank" href="http://trivia.deploydapp.com"><i class="icon-white icon-eye-open"></i> View Online</a>
<a class="btn btn-primary" target="_blank" href="/downloads/examples/trivia.zip"><i class="icon-white icon-download"></i> Download Source</a>

91
docs/index.md Normal file
View File

@@ -0,0 +1,91 @@
# Deployd Guide
Deployd is a new way of building data-driven backends for web apps. Ready-made, configurable *Resources* add common functionality to a Deployd backend, which can be further customized with JavaScript *Events*.
# Getting Started
Create an app by running:
$ dpd create hello
$ cd hello
$ dpd
dpd>
The `dpd>` you see after starting Deployd is a REPL for interacting with the server as it's running.
You will probably use the following commands frequently:
- `open` - Opens your app (`http://localhost:2403` by default) in your default browser
- `dashboard` - Opens your app's Deployd dashboard (`http://localhost:2403/dashboard` by default) in your default browser
To open your app or dashboard immediately after creating an app, put a `--open`/`-o` or `--dashboard`/`-d` flag on `dpd create`:
dpd create hello -d
dpd>
# dpd Command
The `dpd` command line tool has some options that you can specify when you run it:
- `dpd -p [port]` - Runs the Deployd server on a specific port. Default is `2403`.
- `dpd -d` - Runs the Deployd server and immediately runs the `dashboard` command
- `dpd -o` - Runs the Deployd server and immediately runs the `open` command
- `dpd -V` - Outputs the current version of the Deployd server.
- `dpd -h` - Lists the available options in more detail
If you used the Mac or Windows installer, double-clicking on an `app.dpd` file will have the same effect as `dpd -d` - it will start your app and open the dashboard.
# Dashboard
The dashboard is a simple UI that you'll use to create and manage your Deployd backend. You can get to the dashboard by opening `/dashboard` (eg. `http://localhost:2403/dashboard`) in a browser.
The sidebar of the Dashboard lists the Resources that you have in your app. A resource is a feature that you can add to your app's backend.
![Dashboard](/img/docs/dashboard.png)
## Managing Resources
Click on the "+" button on the sidebar to add a resource.
The following resource types are available:
- [Collection](/docs/resources/collection.html)
- [User Collection](/docs/resources/user-collection.html)
From the main view of the dashboard, you can delete and rename resources by clicking on the arrow next to it.'
![Dashboard](/img/docs/dashboard-detail.png)
# Files
Deployd serves static files from its `public` folder. This folder is created when you run `dpd create`. These files will be served with the appropriate cache headers (`Last-Modified` and `Etag`) so browsers will cache them.
Deployd will automatically serve an `index.html` file as the default file in a directory.
# Dpd.js
The Deployd client library (`dpd.js`) can optionally be included in your web app to simplify backend requests. Include it with this script tag in your `<head>` or `<body>`:
<script type="text/javascript" src="/dpd.js" />
This will include a `dpd` object that can be used to make HTTP requests:
dpd.mycollection.post({some: 'value'}, function(result, error) {
//Makes a POST request to /my-collection
});
dpd.mycollection.get({some: 'query'}, function(result, error) {
//Makes a GET request to /my-collection?some=query
});
See the [Dpd.js Reference](/docs/reference/dpdjs.html) for more details.
# More Information
- [Community discussion](/community.html)
- [Github project](https://github.com/deployd/deployd)
- [Issue tracker](https://github.com/deployd/deployd/issues)
- [Deployd blog](http://deployd.tumblr.com/)
- [@deploydapp](https://twitter.com/#!/deploydapp)

View File

@@ -0,0 +1,77 @@
# Advanced Queries
When querying a [Collection](../resources/collection.md), you can use special commands to create a more advanced query.
Deployd supports all of [MongoDB's conditional operators](http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ConditionalOperators); only the common operators and Deployd's custom commands are documented here.
When using an advanced query in REST, you must pass JSON as the query string, for example:
GET /posts?{"likes": {"$gt": 10}}
If you are using dpd.js, this will be handled automatically.
## Comparison ($gt, $lt, $gte, $lte)
Compares a Number property to a given value.
- `$gt` - Greater than
- `$lt` - Less than
- `$gte` - Greater than or equal to
- `$lte` - Less than or equal to
//Finds all posts with more than 10 likes
{
likes: {$gt: 10}
}
## $ne (Not Equal)
The `$ne` command lets you choose a value to exclude.
// Get all posts except those posted by Bob
{
author: {$ne: "Bob"}
}
## $in
The `$in` command allows you to specify an array of possible matches.
// Get articles in the "food", "business", and "technology" categories
{
category: {$in: ["food", "business", "technology"]}
}
# Query commands
Query commands apply to the entire query, not just a single property.
## $sort
The `$sort` command allows you to order your results by the value of a property. The value can be 1 for ascending sort (lowest first; A-Z, 0-10) or -1 for descending (highest first; Z-A, 10-0)
// Sort posts by likes, descending
{
$sort: {likes: -1}
}
## $limit
The `$limit` command allows you to limit the amount of objects that are returned from a query. This is commonly used for paging, along with `$skip`.
// Return the top 10 scores
{
$sort: {score: -1}
$limit: 10
}
## $skip
The `$skip` command allows you to exclude a given number of the first objects returned from a query. This is commonly used for paging, along with `$limit`.
// Return the third page of posts, with 10 posts per page
{
$skip: 20
$limit: 10
}

View File

@@ -0,0 +1,168 @@
# Collection Events Reference
Events allow you to add custom logic to your Collection using JavaScript. Deployd is compatible with ECMAScript 5, so you can use functional-style programming, such as `forEach()`, `map()`, and `filter()`.
### this
The current object is represented as `this`. You can always read its properties. Modifying its properties in an `On Get` request will change the result that the client recieves, while modifying its properties in an `On Post`, `On Put`, or `On Validate` will change the value in the database.
// Example: On Validate
// If a property is too long, truncate it
if (this.message.length > 140) {
this.message = this.message.substring(0, 137) + '...';
}
*Note*: In some cases, the meaning of `this` will change to something less useful inside of a function. If you are using functional programming (i.e. `Array.forEach()`), you may need to bind another variable to `this`:
// Won't work - sum gets set to 0
this.sum = 0;
this.targets.forEach(function(t) {
this.sum += t.points;
});
<!--seperate-->
//Works as expected
var self = this;
this.sum = 0;
this.targets.forEach(function(t) {
self.sum += t.points;
});
### me
The currently logged in User from a User Collection. `undefined` if no user is logged in.
// Example: On Post
// Save the creator's information
if (me) {
this.creatorId = me.id;
this.creatorName = me.name;
}
### query
The query string object. On a specific query (i.e. `/posts/a59551a90be9abd8`), this includes an `id` property.
// Example: On Get
// Don't show the body of a post in a general query
if (!query.id) {
hide(this.body);
}
### cancel()
cancel(message, [statusCode])
Stops the current request with the provided error message and HTTP status code. Status code defaults to `400`. Commonly used for security and authorization.
// Example: On Post
// Don't allow non-admins to create items
if (!me.admin) {
cancel("You are not authorized to do that", 401);
}
### error()
error(key, message)
Adds an error message to an `errors` object in the response. Cancels the request, but continues running the event so it can to collect multiple errors to display to the user. Commonly used for validation.
// Example: On Validate
// Don't allow certain words
// Returns response {"errors": {"name": "Contains forbidden words"}}
if (!this.name.match(/(foo|bar)/)) {
error('name', "Contains forbidden words");
}
### hide()
hide(property)
Hides a property from the response.
// Example: On Get
// Don't show private information
if (!me || me.id !== this.creatorId) {
hide('secret');
}
### protect()
protect(property)
Prevents a property from being updated.
// Example: On Put
// Protect a property
protect('createdDate');
### emit()
emit([userCollection, query], event, [data])
Emits a realtime event to the client
You can use `userCollection` and `query` parameters to limit the event broadcast to specific users.
// Example: On Put
// Alert the owner that their post has been modified
if (me.id !== this.creatorId) {
emit(dpd.users, {id: this.creatorId}, 'postModified', this);
}
<!--seperate-->
// Example: On Post
// Alert clients that a new post has been created
emit('postCreated', this);
In the front end:
// Listen for new posts
dpd.on('postCreated', function(post) {
//do something...
});
See the [Dpd.js Reference](/docs/reference/dpdjs.html#docs-realtime) for details on how to listen for events.
### dpd
The entire [dpd.js](/docs/reference/dpdjs.html) library, except for `dpd.on()`, is available from events. It will also properly bind `this` in callbacks.
// Example: On Get
// If specific query, get comments
dpd.comments.get({postId: this.id}, function(results) {
this.comments = results;
});
<!--seperate-->
// Example: On Delete
// Log item elsewhere
dpd.archived.post(this);
Dpd.js will prevent recursive queries. This works by returning `null` from a `dpd` function call that has already been called further up in the stack.
// Example: On Get /recursive
// Call self
dpd.recursive.get(function(results) {
if (results) this.recursive = results;
});
<!--seperate-->
// GET /recursive
{
"id": "a59551a90be9abd8",
"recursive": [
{
"id": "a59551a90be9abd8"
}
]
}
### console.log()
console.log([arguments]...)
Logs the values provided to the command line. Useful for debugging.

91
docs/reference/dpdjs.md Normal file
View File

@@ -0,0 +1,91 @@
# dpd.js
The Deployd client library (`dpd.js`) can optionally be included in your web app to simplify backend requests. Include it with this script tag in your `<head>` or `<body>`:
<script type="text/javascript" src="/dpd.js" />
This will include a `dpd` object that contains all of the resources on the server. For example, if your app contains a `/my-objects` and a `/users` resource, you would use `dpd.myobjects` and `dpd.users` to access their APIs.
Alternatively, you can use `dpd` as a function, such as `dpd('my-objects')` or `dpd('users')`, but this will not populate any resource-specific helper functions.
# Realtime
dpd.on(event, fn)
Listens for an event coming from the server.
* `event` - The name of the event to listen for
* `fn` - Callback `function(eventData)`. Called every time the event is received.
<!--seperate-->
// Listen for a new post
dpd.on('postCreated', function(post) {
//do something
});
In your Collection Events:
// On Post
emit('postCreated', this);
See the [Collection Event Reference](/docs/reference/collection-events.html#docs-emit) for details on how to send events with the `emit()` function.
# Generic Resource APIs
**Note**: These APIs are designed to work with any resource, so some features may be unavailable depending on the resource. Use resource-specific documentation to learn how to use their APIs.
### get()
dpd.[resource].get([func], [path], [query], fn)
Makes a GET HTTP request at the URL `/<resource>/<func>/<path>`, using the `query` object as the query string if provided.
- `func` - A special RPC identifier, i.e. `/me`.
- `path` - An idenitifier for a particular object, usually the id
- `query` - An object defining the querystring. If the object is complex, it will be serialized as JSON.
- `fn` - Callback `function(result, error)`.
### post()
dpd.[resource].post([path], [query], body, fn)
Makes a POST HTTP request at the URL `/<resource>/<path>`, using the `query` object as the query string if provided and `body` as the request body.
- `path` - An idenitifier for a particular object, usually the id
- `query` - An object defining the querystring. If the object is complex, it will be serialized as JSON.
- `body` - The body of the request; will be serialized as JSON as sent with `Content-Type: application/json` header.
- `fn` - Callback `function(result, error)`.
### put()
dpd.[resource].put([path], [query], body, fn)
Makes a PUT HTTP request at the URL `/<resource>/<path>`, using the `query` object as the query string if provided and `body` as the request body.
- `path` - An idenitifier for a particular object, usually the id
- `query` - An object defining the querystring. If the object is complex, it will be serialized as JSON and passed as the `q` parameter.
- `body` - The body of the request; will be serialized as JSON as sent with `Content-Type: application/json` header.
- `fn` - Callback `function(result, error)`.
### del()
dpd.[resource].del([path], [query], fn)
Makes a DELETE HTTP request at the URL `/<resource>/<path>`, using the `query` object as the query string if provided.
- `path` - An idenitifier for a particular object, usually the id
- `query` - An object defining the querystring. If the object is complex, it will be serialized as JSON and passed as the `q` parameter.
- `fn` - Callback `function(result, error)`.
### exec()
dpd.[resource].exec(func, [path], [body], fn)
Makes an RPC-style POST HTTP request at the URL `/<resource>/<func>/<path>`. Useful for functions that don't make sense in REST-style APIs, such as `/users/login`.
- `func` - The name of the RPC to call
- `path` - An idenitifier for a particular object, usually the id
- `body` - The body of the request; will be serialized as JSON as sent with `Content-Type: application/json` header.
- `fn` - Callback `function(result, error)`.

View File

@@ -0,0 +1,58 @@
# Update Modifiers
When updating an object in a [Collection](/docs/resources/collection.html), you can use special modifier commands to more granularly change property values.
## $inc
The `$inc` command increments the value of a given Number property.
// Give a player 5 points
{
score: {$inc: 5}
}
## $push
The `$push` command adds a value to an Array property.
// Add a follower to a user by storing their id.
{
followers: {$push: 'a59551a90be9abd8'}
}
*Note: This will work even on an undefined property*
## $pushAll
The `$pushAll` command adds multiple values to an Array property.
// Add mentions of users
{
mentions: {
$pushAll: ['a59551a90be9abd8', 'd0be45d1445d3809']
}
}
*Note: This will work even on an undefined property*
## $pull
The `$pull` command removes a value from an Array property.
// Remove a user from followers
{
followers: {$pull: 'a59551a90be9abd8'}
}
*Note: If there is more than one matching value in the Array, this will remove all of them*
## $pullAll
The `$pullAll` command removes multiple values from an Array property.
// Remove multiple users
{
followers: {$pullAll: ['a59551a90be9abd8', 'd0be45d1445d3809']}
}
*Note: This will remove all of matching values from the Array*

View File

@@ -0,0 +1,185 @@
# Collection
The Collection resource allows clients to save, update, delete, and query data of a given type.
## Properties
![Properties editor](/img/docs/collection-properties.png)
Properties in a Collection describe the objects that it can store.
Every Collection has an `id` property that cannot be removed. This property is automatically generated when you create an object and serves as a unique identifier.
You can add these types of properties to a Collection:
- `String` - Acts like a JavaScript string
- `Number` - Stores numeric values, including floating points.
- `Boolean` - Either true or false. (To avoid confusion, Deployd will consider null or undefined to be false)
- `Object` - Stores any JSON object. Used for storing arbitrary data on an object without needing to validate schema.
- `Array` - Stores an array of any type.
*Deprecated* Any property can be marked as "Required". This will cause an error message if the property is null or undefined when an object is created.
## Events
Events allow you to add custom logic to your Collection. Events are written in JavaScript, see the [Collection Events Reference](/docs/reference/collection-events.html) for details on the API.
These events are available for scripting:
### On Get
Called whenever an object is loaded from the server. Commonly used to hide properties, restrict access to private objects, and calculate dynamic values.
// Example On Get: Hide Secret Properties
if (!me || me.id !== this.creatorId) {
hide('secret');
}
<!--seperate-->
// Example On Get: Load A Post's Comments
dpd.comments.get({postId: this.id}, function(comments) {
this.comments = comments;
});
*Note: When a list of objects is loaded, `On Get` will run once for each object in the list. Calling `cancel()` in this case will remove the object from the list, rather than cancelling the entire request.*
### On Validate
Called whenever an object's values change, including when it is first created. Commonly used to validate property values and calculate certain dynamic values (i.e. last modified time).
// Example On Validate: Enforce a max length
if (this.body.length > 100) {
error('body', "Cannot be more than 100 characters");
}
<!--seperate-->
// Example On Validate: Normalize an @handle
if (this.handle.indexOf('@') !== 0) {
this.handle = '@' + this.handle;
}
*Note: `On Post` or `On Put` will execute after `On Validate`, unless `cancel()` or `error()` is called*
### On Post
Called when an object is created. Commonly used to prevent unauthorized creation and save data relevant to the creation of an object, such as its creator.
// Example On Post: Save the date created
this.createdDate = new Date();
<!--seperate-->
// Example On Post: Prevent unauthorized users from posting
if (!me) {
cancel("You must be logged in", 401);
}
### On Put
Called when an object is updated. Commonly used to restrict editing access to certain roles, or to protect certain properties from editing.
// Example On Put: Protect readonly/automatic properties
protect('createdDate');
protect('creatorId')
### On Delete
Called when an object is deleted. Commonly used to prevent unauthorized deletion.
// Example On Delete: Prevent non-admins from deleting
if (!me || me.role !== 'admin') {
cancel("You must be an admin to delete this", 401);
}
## API
A Collection allows clients to interact with it over a REST interface, as well as with the `dpd.js` library.
### Listing Data
**REST Example**
GET /todos
**dpd.js Example**
// Get all todos
dpd.todos.get(function(results, error) {
//Do something
});
### Querying Data
Filters results by the property values specified.
**REST Example**
GET /todos?category=red
**dpd.js Example**
// Get all todos that are in the red category
dpd.todos.get({category: 'red'}, function(results, error) {
//Do something
});
*Note: for Array properties, this acts as a "contains" operation. For example, the above query would also match `category` value of `["blue", "red", "orange"]`.*
Use the [Advanced Query commands](/docs/reference/advanced-queries.html) for more control over the results.
### Creating an Object
**REST Example**
POST /todos
{"title": "Walk the dog", "category": "red"}
**dpd.js Example**
dpd.todos.post({title: 'Walk the dog'}, function(result, error) {
//Do something
});
### Getting a Single Object
**REST Example**
GET /todos/add1ad66465e6890
**dpd.js Example**
dpd.todos.get('add1ad66465e6890', function(result, error)) {
//Do something
});
### Updating an Object
You do not need to provide all properties for an object. Deployd will only update the properties you provide.
You can use [Update Modifiers](/docs/reference/modifiers.html) for atomic updates such as incrementing Number properties and adding to Array properties.
**REST Example**
PUT /todos/add1ad66465e6890
{"title": "Bathe the cat"}
**dpd.js Example**
dpd.todos.put('add1ad66465e6890', {title: "Bathe the cat"}, function(result, error)) {
//Do something
});
### Deleting an Object
**REST Example**
DELETE /todos/add1ad66465e6890
**dpd.js Example**
dpd.todos.del('add1ad66465e6890', function(result, error)) {
//Do something
});

View File

@@ -0,0 +1,213 @@
# Custom Resources
A resource is a node module that deployd mounts at a given url and handles HTTP requests. Deployd comes bundled with two resources: [Collection](collection.html) and [UserCollection](user-collection.html). You can create your own custom resources by extending the `Resource` constructor and implementing a `handle()` method. Here is an example of a simple custom resource:
var Resource = require('deployd/lib/resource')
, util = require('util');
function Hello(options) {
Resource.apply(this, arguments);
}
util.inherits(Hello, Resource);
module.exports = Hello;
Hello.prototype.handle = function (ctx, next) {
if(ctx.req && ctx.req.method !== 'GET') return next();
ctx.done(null, {hello: 'world'});
}
This resource will respond to every GET request with: `{"hello": "world"}`.
## Loading / File Structure
Deployd looks for custom resources in your project's `node_modules` folder. Any node module that exports a constructor that inherits from `Resource` will be loaded and made available in the dashboard.
Heres an example project structure:
- my-project
- app.dpd
- resources
- data
- node_modules
- hello.js
- my-module
- index.js
- README.md
- package.json
- node_modules
- foo-module
- foo.js
Resources can be a single file (eg. hello.js) or a folder with a `package.json` and its own `node_modules` folder. Resources are just regular node modules.
## Dashboard
If your custom resource type is properly installed in your app, you should see it in the Create Resource menu in the dashboard. To customize its appearance in this menu, use the following properties:
// The resource type's name as it appears in the dashboard.
// If this is not set, it will appear with its constructor
// name ('Hello' in this case)
Hello.label = 'Hello World';
// The default path suggested to users creating a resource.
// If this is not set, it will use the constructor's name
// in lowercase. ('/hello' in this case).
Hello.defaultPath = '/hello-world';
By default, the dashboard will provide a JSON editor to configure this resource. For a more customized experience, you can set specific properties to be edited:
Hello.basicDashboard = {
settings: [{
name: 'propertyName',
type: 'text',
description: "This description appears below the text field"
}, {
name: 'longTextProperty',
type: 'textarea'
}, {
name: 'numericProperty',
type: 'number'
}, {
name: 'booleanProperty',
type: 'checkbox'
}]
};
![Basic Dashboard](/img/docs/basic-dashboard.png)
## Context
Resources must implement a `handle(ctx, next)` method. This method is passed a `Context` during HTTP requests. The resource can either handle this context and call `ctx.done(err, obj)` with an error or result JSON object or call `next()` to give the context back to the router. If a resource calls `next()` the router might find another match for the resource, or respond with a `404`.
A context comes with several useful properties to make HTTP easy.
- **query** the requests query as an object
- **body** the requests body as JSON if it exists
- **session** the current user's session if one exists
- **dpd** the internal interface for interacting with other resources
## Script
To make your `Resource` reusable, you can expose hooks to execute scripts when a resource is handling a request. A `Script` runs JavaScript in an isolated context. It interfaces with the current request through a `domain` which is passed to a `Script` to run.
For example, in the `Collection` resource, custom logic is injected through hooks called **event scripts**. These are short scripts that are executed in their own context. They do not share a scope or state with any other scripts. In an **event script** the global object contains a set of **domain functions**. These functions, such as `hide()`, `error()`, and `protect()` operate on the context. In the case of a `Collection` they interact with the item that is being retrieved or saved, the `ctx.body`.
A common type of `Script` is an event. The following example resource loads an event.
my-resource.js:
var Resource = require('deployd/lib/resource');
var Script = require('deployd/lib/script');
var fs = require('fs');
var util = require('util');
var MyResource = function () {
Resource.apply(this, arguments);
}
util.inherits(MyResource, Resource);
MyResource.events = ["get"]; // Registers events to be loaded. Also makes them editable in the dashboard
MyResource.prototype.handle = function (ctx) {
var value;
var domain = {
send: function(msg) {
value = msg;
},
};
this.events.get.run(ctx, domain, function() {
ctx.done(null, value);
});
}
get.js:
send({hello: 'world'});
GET /my-resource response:
{
"hello": "world"
}
## Custom Dashboard
To create a fully customized editor for your resource, set the "dashboard" property:
Hello.dashboard = {
path: __dirname + '/dashboard', //The absolute path to your front-end files
pages: ["Credentials", "Events", "API"], // Optional; these pages will appear on the sidebar.
scripts: [
'/js/lib/backbone.js', //relative paths to extra JavaScript files you would like to load
'/js/lib/jquery-ui.js'
]
};
This will load your resources' editor from the dashboard path. It will load the following files:
- `[current-page].html`
- `js/[current-page].js`
- `style.css`
The default page is `index`; the `config` page will also redirect to `index`. The `events` page will load the default event editor if no `events.html` file is provided.
To embed the event editor in your dashboard, include this empty div:
<div id="event-editor" class="default-editor"></div>
For styling, the dashboard uses a reskinned version of [Twitter Bootstrap 2.0.2](http://twitter.github.com/bootstrap/).
The dashboard provides several JavaScript libraries by default:
- [jQuery 1.7.2](http://jquery.com/)
- [jquery.cookie](https://github.com/carhartl/jquery-cookie/)
- [Underscore 1.3.3](http://underscorejs.org/)
- [Twitter Bootstrap 2.0.2](http://twitter.github.com/bootstrap/javascript.html)
- [UIKit](http://visionmedia.github.com/uikit/)
- [Ace Editor](https://github.com/ajaxorg/ace) (noconflict version)
-- JavaScript mode
-- JSON mode
-- Custom theme for the Dashboard (`ace/theme/deployd`)
- [Google Code Prettify](http://code.google.com/p/google-code-prettify/)
- dpd.js
Within the dashboard, a `Context` object is available:
//Automatically generated by Deployd:
window.Context = {
resourceId: '/hello', // The id of the current resource
resourceType: 'Hello', // The type of the current resource
page: 'properties', // The current page, in multi page editors
basicDashboard: {} // The configuration of the basic dashboard - not ordinarily useful
};
You can use this to query the current resource:
dpd(Context.resourceId).get(function(result, err) {
//Do something
});
In the dashboard, you also have access to the special `__resources` resource, which lets you update your app's configuration files:
// Get the config for the current resource
dpd('__resources').get(Context.resourceId, function(result, err) {
//Do something
});
// Set a property for the current resource
dpd('__resources').put(Context.resourceId, {someProperty: true}, function(result, err) {
//Do something
});
// Set all properties for the current resource, deleting any that are not provided
dpd('__resources').put(Context.resourceId, {someProperty: true, $setAll: true}, function(result, err) {
//Do something
});
// Save another file, which will be loaded by the resource
dpd('__resources').post(Context.resourceId + '/content.md', {value: "# Hello World!"}, function(result, err)) {
//Do something
});

View File

@@ -0,0 +1,63 @@
# User Collection
A User Collection extends a [Collection](/docs/resources/collection.html), adding the functionality needed to authenticate users with your app.
## Properties
User Collections can have the same properties as a Collection, with two additional non-removable properties:
- `username` - The user's identifier; must be unique.
- `password` - A write-only, encrypted password
## API
User Collections add three new methods to the standard Collection API:
### Logging in
Log in a user with their username and password. If successful, the browser will save a secure cookie for their session. This request responds with the session details:
{
"id": "s0446b993caaad577a..." //Session id - usually not needed
"path": "/users" // The path of the User Collection - useful if you have different types of users.
"uid": "ec54ad870eaca95f" //The id of the user
}
**REST Example**
POST /users/login
{"username": "test@test.com", "password": "1234"}
**dpd.js Example**
dpd.users.login({'username': 'test@test.com', 'password': '1234'}, function(result, error) {
//Do something
});
### Logging out
Logging out will remove the session cookie on the browser and destroy the current session. It does not return a result.
**REST Example**
POST /users/logout
**dpd.js Example**
dpd.users.logout(function(result, error) {
//Do something
});
### Getting the current user
Returns the user that is logged in.
**REST Example**
GET /users/me
**dpd.js Example**
dpd.users.me(function(result, errors) {
//Do something
});

View File

@@ -0,0 +1,205 @@
# Building a Comments app
In this tutorial, you'll see how to create a simple app from the ground up in Deployd. This tutorial assumes a working knowledge of jQuery. It doesn't assume any knowledge of Deployd, but it's recommended to read the [Hello World](hello-world.md) tutorial if you haven't already.
## Getting started
Create a new app in the command line:
$ dpd create comments
$ cd comments
Using your text editor of choice, replace the default `index.html` file in the `public` folder:
<!DOCTYPE html>
<html>
<head>
<title>Deployd Tutorial</title>
<style type="text/css">
body { font-size: 16pt; }
.container { width: 960px; margin-left: auto; margin-right: auto; }
form { border: #cccccc 1px solid; padding: 20px; margin-bottom: 10px; -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; }
.form-element { margin-bottom: 10px; }
#refresh-btn { margin-bottom: 20px; }
.comment { padding: 10px; margin-bottom: 10px; border-bottom: #cccccc 1px solid; }
.comment .links { float: right; }
.comment .links a { margin-left: 10px; }
.comment .author { font-style: italic; }
</style>
</head>
<body>
<div class="container">
<div id="comments">
</div>
<form id="comment-form">
<div class="form-element">
<label for="name">Name: </label>
<input type="text" id="name" name="name" />
</div>
<div class="form-element">
<textarea id="comment" name="comment" rows="5" cols="50">
</textarea>
</div>
<div class="form-element">
<button type="submit">Add New Comment</button>
</div>
</form>
</div>
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript" src="script.js"></script>
</body>
</html>
Also add a file `script.js` and paste this code:
$(document).ready(function() {
$('#comment-form').submit(function() {
//Get the data from the form
var name = $('#name').val();
var comment = $('#comment').val();
//Clear the form elements
$('#name').val('');
$('#comment').val('');
addComment({
name: name,
comment: comment
});
return false;
});
function addComment(comment) {
$('<div class="comment">')
.append('<div class="author">Posted by: ' + comment.name + '</div>')
.append('<p>' + comment.comment + '</p>')
.appendTo('#comments');
}
});
Run the app:
$ dpd --open
dpd>
The `open` command will automatically open `http://localhost:2403` in your browser.
![App Preview](/img/tutorials/comments-app-preview.png)
This basic app asks for a name and message body to post a comment. Take a moment to read the code and see how it works.
Next, we'll add a Deployd backend to this app, so that users can interact with each other and post comments.
## Creating a backend
Open the dashboard by by typing `dashboard` into the `dpd>` prompt.
1. Create a new Collection Resource and call it `/comments`.
2. On the Properties editor, add two "string" properties called `name` and `comment`.
3. In the Data editor, add a couple of comments so you can start testing right away.
That's all you have to do in the backend for now!
## Integrating in the frontend
In `index.html`, add the following script reference in between jQuery and `script.js` (near line 37):
<script type="text/javascript" src="/dpd.js"></script>
This will add a reference to [dpd.js](/docs/reference/dpdjs.html), a simple library dynamically built specifically for your app's backend. Dpd.js will automatically detect what resources you have added to your app and add them to the `dpd` object. Each resource object has asynchronous functions to communicate with your Deployd app.
In `script.js`, add a `loadComments()` function inside of `$(document).ready`:
function loadComments() {
dpd.comments.get(function(comments, error) { //Use dpd.js to send a request to the backend
$('#comments').empty(); //Empty the list
comments.forEach(function(comment) { //Loop through the result
addComment(comment); //Add it to the DOM.
});
});
}
And call it when the page loads:
$(document).ready(function() {
loadComments();
//...
});
If you run the app now, you should see the comments that you created in the Dashboard.
The `get` function that makes this work sends an HTTP `GET` request to `/comments`, and returns an array of objects in the resource. There's nothing magical hapenning in dpd.js; you can use standard AJAX or HTTP requests if you prefer, or if you're not working in JavaScript (i.e. mobile apps)
**Note**: If you haven't used AJAX much, note that all dpd.js functions are *asynchronous* and don't directly return a value.
//Won't work:
var comments = dpd.comments.get();
This means that your JavaScript will continue to execute and respond to user input while data is loading, which will make your app feel much faster to your users.
## Saving data
Notice that any comments you add through the app's form are still gone when you refresh. Let's make the form save comments to the database.
Delete these lines from `script.js` (near line 10, depending on where you put your `loadComments()` function):
//Clear the form elements
$('#name').val('');
$('#comment').val('');
addComment({
name: name,
comment: comment
});
And replace them with:
dpd.comments.post({
name: name,
comment: comment
}, function(comment, error) {
if (error) return showError(error);
addComment(comment);
$('#name').val('');
$('#comment').val('');
});
Add a utility function at the very top of `script.js` to alert any errors we get:
function showError(error) {
var message = "An error occured";
if (error.message) {
message = error.message;
} else if (error.errors) {
var errors = error.errors;
message = "";
Object.keys(errors).forEach(function(k) {
message += k + ": " + errors[k] + "\n";
});
}
alert(message);
}
An `error` object can include either a `message` property or an `errors` object containing validation errors.
If you load the page now, you should be able to submit a comment that appears even after you refresh.
## Conclusion
In this tutorial, you saw how to create a simple app in Deployd. In the next part (coming soon), you'll see how to secure this app with Events.
The source code for this chapter includes a few extra features. If you're feeling adventurous, try adding them yourself:
- A refresh button that reloads the comments without refreshing the page
- Edit and Delete links next to each comment. Hint: use the `put()` and `del()` functions from dpd.js.
<a class="btn btn-primary" href="http://deployd.com/downloads/tutorials/dpd-comments-1.zip"><i class="icon-white icon-download"></i> Download Source</a>

View File

@@ -0,0 +1,99 @@
# Hello World
In this tutorial, you'll get a taste of how to use Deployd by creating a simple backend.
## Creating an app
Start out by creating a Deployd app. Open a command line in a directory of your choice and type:
$ dpd create hello -d
This will create your first Deployd app in a folder called `hello` and open up the Deployd *Dashboard*. If you open up the folder and look at the contents, you'll see the following files and folders:
- `.dpd` is an internal folder that contains housekeeping information.
- `data` contains your app's database.
- `public` contains all of the static web assets that you'd like to host.
- `resources` contains your application's resource configuration.
- `app.dpd` is your app's settings.
## Dashboard
The Dashboard is where you will create the *resources* that make up your app's backend. A resource is essentially a feature that your frontend needs to access.
This new app doesn't contain any resources yet, so add one now by clicking on "Resources +" and choosing Collection. Click "Create" (leave it at its default name of `my-objects`).
![Dashboard](http://deployd.com/img/tutorials/hello-world-dashboard.png)
You have just added your first resource to Deployd!
## Collections
The dashboard should open up the Collection Property editor.
![Properties](http://deployd.com/img/tutorials/hello-world-properties.png)
This is where you define the objects that you want to store in this Collection. For now, make sure `string` is selected as the type and enter `name` as the property. Click "Add". This means that every object stored in the `my-objects` collection will have a `name` property.
Click on "Data" in the sidebar. This will open up the Collection Data editor. Type "World" in the `name` field and click "Add". Now the `my-objects` collection has an object in it.
Let's see how this will look to your app's frontend code. Open up a new browser tab and navigate to `http://localhost:2403/my-objects`. (If your browser tries to download it as a file, open it with any text editor.) You should see something like this (your "id" will be different):
[
{
"name":"World",
"id":"a59551a90be9abd8"
}
]
This is a JSON array of objects. If you add another object to the collection, it will look like this:
[
{
"name":"World",
"id":"a59551a90be9abd8"
}, {
"name":"Joe",
"id":"d0be45d1445d3809"
}
]
If you copy one of the ids and put it at the end of the URL (i.e. `/my-objects/a59551a90be9abd8`), you will see just that object:
{
"name":"World",
"id":"a59551a90be9abd8"
}
Collections allow you to access data on the backend with very little setup.
## Events
Go back to the Dashboard and click on the "Events" link in the sidebar. Select the "On Get" tab and type the following JavaScript:
this.greeting = "Hello, " + this.name + "!";
If you check the data again, you will see that value set on the objects:
[
{
"name":"World","id":"a59551a90be9abd8",
"greeting":"Hello, World!"
}, {
"name":"Joe",
"id":"d0be45d1445d3809",
"greeting":"Hello, Joe!"
}
]
Events allow you to customize the behavior of data in a collection with simple JavaScript.
## Next Steps
This was just a quick tour through Deployd. To learn more:
- If you have an application that can send raw HTTP requests, you could try to save data using POST and PUT verbs. Make sure to add a `Content-Type: application/json` header.
- Check out the API link on the sidebar and see how to access this data from your frontend JavaScript. Try building a simple app in the `public` folder.
- Check out [the community](/community.html) and ask questions about Deployd.
- Start reading the [Comments App](/docs/tutorials/comments-1.html) tutorial to see how to make a full app
<a class="btn btn-primary" href="http://deployd.com/downloads/tutorials/dpd-hello-world.zip"><i class="icon-white icon-download"></i> Download Source</a>

View File

@@ -1,5 +1,6 @@
var Server = require('./lib/server')
, upgrade = require('doh').upgrade;
, upgrade = require('doh').upgrade
, Monitor = require('./lib/monitor');
/**
* export a simple function that constructs a dpd server based on a config
@@ -9,4 +10,10 @@ module.exports = function (config) {
var server = new Server(config);
upgrade(server);
return server;
};
};
/**
* opt-in process monitoring support
*/
module.exports.createMonitor = Monitor.createMonitor;

View File

@@ -1,15 +1,14 @@
var fs = require('fs')
, path = require('path')
, Resource = require('./resource')
, loadTypes = require('./type-loader')
, _loadTypes = require('./type-loader')
, InternalResources = require('./resources/internal-resources')
, Files = require('./resources/files')
, ClientLib = require('./resources/client-lib')
, Dashboard = require('./resources/dashboard')
, debug = require('debug')('config-loader')
, ignore = {}
, domain = require('domain');
, domain = require('domain')
, async = require('async');
/*!
* Loads resources from a project folder
* Callback receives two arguments `(err, resources)`.
@@ -18,157 +17,145 @@ var fs = require('fs')
* @param {Function} callback
*/
module.exports.loadConfig = function(basepath, server, fn) {
var remaining = 0
, finished = false
, resourceConfig = resourceConfig || {}
, resources = server.__resourceCache || []
, src = {}
, error;
var resources = server.__resourceCache || [];
server.__resourceCache = null;
if(resources.length) {
if (resources.length) {
debug("Loading from cache");
fn(null, resources);
if(server.options.env === 'development') {
// dump the cache in two seconds
setTimeout(function () {
delete server.__resourceCache;
}, 2000);
}
return;
}
function done() {
if(!finished && !remaining) {
if(error) return fn(error);
var getTypes = async.memoize(loadTypes);
remaining = -1;
finished = true;
var InternalResources = require('./resources/internal-resources');
debug('done, adding internals');
var clientLib = new ClientLib('dpd.js', { config: { resources: resources }, server: server});
clientLib.load(function(err) {
if (err) return fn(err);
resources.push(new Files('', { config: { 'public': './public' }, server: server }));
resources.push(clientLib);
resources.push(new InternalResources('__resources', {config: {configPath: basepath}, server: server}));
resources.push(new Dashboard('dashboard', {server: server}));
server.__resourceCache = resources;
fn(null, resources);
});
async.waterfall([
async.apply(loadResourceDir, basepath)
, async.apply(loadResources, getTypes, basepath, server)
, async.apply(addInternalResources, server, basepath)
], function(err, result) {
if (server.options && server.options.env !== 'development') {
server.__resourceCache = result;
}
}
fn(err, result);
});
};
loadTypes(function(defaults, types) {
function loadTypes(fn) {
_loadTypes(function(defaults, types) {
Object.keys(types).forEach(function(key) {
defaults[key] = types[key];
});
types = defaults;
loadResources(types);
types = defaults;
fn(null, types);
});
}
function loadResource(name, path, config, types) {
debug("Loading resource: %s", name);
var type = config.type
, resource
, o = {
config: config
, server: server
, db: server.db
, configPath: path
};
if (types[type]) {
var d = domain.create();
d.on('error', function (err) {
err.message += ' - when initializing: ' + o.config.type;
console.error(err.stack || err);
process.exit();
});
remaining++;
d.run(function () {
process.nextTick(function () {
remaining--;
resource = new types[o.config.type](name, o);
if (resource.load) {
remaining++;
resource.load(function(err) {
remaining--;
if (err) {
error = err;
return done();
}
resources.push(resource);
done();
});
} else {
resources.push(resource);
}
function loadResourceDir(basepath, fn) {
var dir = path.join(basepath, 'resources');
async.waterfall([
function(fn) {
fs.readdir(dir, fn);
},
function(results, fn) {
async.filter(results, function(file, fn) {
fs.stat(path.join(dir, file), function(err, stat) {
fn(stat && stat.isDirectory());
});
}, function(results) {
fn(null, results);
});
} else {
error = 'cannot find type ' + o.config.type + ' for resource ' + name;
}
if(error) throw error;
}
function loadResources(types) {
fs.readdir(path.join(basepath, 'resources'), function (err, dir) {
if(dir && dir.length) {
dir.forEach(function (file) {
if(!ignore[file]) {
remaining++;
fs.stat(path.join(basepath, 'resources', file), function (err, stat) {
remaining--;
if(err) throw err;
if(stat && stat.isDirectory()) {
var spath = path.join(basepath, 'resources', file, 'config.json')
, resource = file;
(fs.exists || path.exists)(spath, function (exists) {
if(exists) {
remaining++;
fs.readFile(spath, 'utf-8', function(err, data) {
remaining--;
var settings;
if(err) throw err;
try {
settings = JSON.parse(data);
loadResource(file, path.join(basepath, 'resources', file), settings, types);
} catch(e) {
if(err) throw err;
}
done();
});
} else {
done();
}
});
} else {
done();
}
});
}
});
} else {
done();
}
});
}
], fn);
};
}
function loadResources(getTypes, basepath, server, files, fn) {
async.map(files, function(resourceName, fn) {
var resourcePath = path.join(basepath, 'resources', resourceName);
var configPath = path.join(resourcePath, 'config.json');
async.auto({
types: function(fn) {
getTypes(fn);
},
configJsonFile: function(fn) {
debug("reading %s", configPath);
fs.readFile(configPath, 'utf-8', fn);
},
configJson: ['configJsonFile', function(fn, results) {
try {
var settings = JSON.parse(results.configJsonFile);
fn(null, settings);
} catch (ex) {
fn(ex);
}
}],
instance: ['configJson', 'types', function(fn, results) {
debug("Loading resource: %s", resourceName);
var config = results.configJson
, types = results.types
, type = config.type
, resource
, o;
o = {
config: config
, server: server
, db: server.db
, configPath: resourcePath
};
if (!types[type]) return fn(new Error("Cannot find type \"" + type + "\" for resource " + resourceName));
var d = domain.create();
d.on('error', function (err) {
err.message += ' - when initializing: ' + o.config.type;
console.error(err.stack || err);
process.exit();
});
d.run(function() {
process.nextTick(function() {
resource = new types[type](resourceName, o);
loadResourceExtras(resource, fn);
});
});
}]
}, function(err, results) {
if (err && err.code === 'ENOENT') {
err = new Error("Expected file: " + path.relative(basepath, err.path));
}
fn(err, results && results.instance);
});
}, fn);
}
function loadResourceExtras(resource, fn) {
async.series([
function(fn) {
if (resource.load) {
resource.load(fn);
} else {
fn();
}
}
], function(err) {
fn(err, resource);
});
}
function addInternalResources(server, basepath, resources, fn) {
var internals = [
new Files('', { config: { 'public': './public' }, server: server })
, new ClientLib('dpd.js', { config: { resources: resources }, server: server})
, new InternalResources('__resources', {config: {configPath: basepath}, server: server})
, new Dashboard('dashboard', {server: server})
];
async.forEach(internals, loadResourceExtras, function(err) {
fn(err, resources.concat(internals));
});
}

View File

@@ -93,7 +93,11 @@ Context.prototype.done = function(err, res) {
try {
if(status != 204 && status != 304) {
if(body && body.length) this.res.setHeader('Content-Length', body.length);
if(body) {
this.res.setHeader('Content-Length', Buffer.isBuffer(body)
? body.length
: Buffer.byteLength(body));
}
this.res.setHeader('Content-Type', type);
this.res.end(body);
} else {

View File

@@ -114,7 +114,7 @@ function getConnection(db, fn) {
fn(null, db._mdb);
} else if(db.connecting) {
db.once('connection attempted', function (err) {
fn(err, db._mdb)
fn(err, db._mdb);
});
} else {
db.connecting = true;

157
lib/monitor.js Normal file
View File

@@ -0,0 +1,157 @@
var ForeverMonitor = require('forever-monitor').Monitor
, util = require('util')
, uuid = require('./util/uuid')
, keypress = require('keypress')
, EventEmitter = require('events').EventEmitter;
function Monitor(script, options) {
options = options || {};
options.fork = true;
ForeverMonitor.call(this, script, options);
}
util.inherits(Monitor, ForeverMonitor);
module.exports = Monitor;
function createCommands(monitor, data, restarting, fn) {
var commands = {};
if(data.createCommands) {
Object.keys(data.createCommands).forEach(function (cmd) {
commands[cmd] = function () {
var fn
, fnIndex
, args = Array.prototype.slice.call(arguments)
, finalArgs = [];
args.forEach(function (val) {
if(typeof val == 'function') {
fn = val;
} else {
finalArgs.push(val);
}
});
monitor.exec(cmd, finalArgs, fn);
};
});
fn.call(monitor, null, commands, restarting);
}
}
Monitor.prototype.start = function (fn) {
var start = ForeverMonitor.prototype.start
, monitor = this
, restarting = arguments[0] === true
, startCallback = this.startCallback;
if(typeof fn === 'function') {
startCallback = this.startCallback = fn;
start.call(this);
} else {
start.apply(this, arguments);
}
if(!(this.child.stdout && this.child.stderr)) {
this.child.stdout = process.stdout;
this.child.stderr = process.stderr;
}
if(this.startCallback) {
this.child.once('message', function (data) {
createCommands(monitor, data, restarting, startCallback);
});
}
}
Monitor.prototype.exec = function (cmd, args, fn) {
var ticket = uuid.create()
, monitor = this;
this.child.send({command: cmd, args: args, ticket: ticket});
this.once(ticket, function (data) {
if(data.command === cmd) {
fn && fn.apply(monitor, data.args);
}
});
this.child.on('message', function (data) {
if(data.command && data.ticket) {
monitor.emit(data.ticket, data);
}
});
}
Monitor.createCommands = function (commands) {
var funcs = {}
, ctx = this;
process.on('message', function (data) {
var cmd = data.command
, fn = funcs[cmd];
if(typeof fn === 'function') {
var args = data.args;
args.push(function () {
process.send({command: cmd, args: Array.prototype.slice.call(arguments), ticket: data.ticket});
});
fn.apply(ctx, args);
}
});
Object.keys(commands).forEach(function (cmd) {
funcs[cmd] = commands[cmd];
commands[cmd] = true;
});
process.send({createCommands: commands});
}
Monitor.createMonitor = function (config) {
var opts = {stdio: ['pipe', process.stdout, process.stderr, 'ipc']}
, monitor = new Monitor(__dirname + '/start.js', opts)
, server = new EventEmitter();
server.options = config;
server.listen = function () {
monitor.start(function (err, commands, restarting) {
keypress(process.stdin);
commands.start(config, function (err) {
if(err) {
server.emit('error', err);
} else if(!restarting) {
server.emit('listening');
}
});
});
return server;
};
monitor.on('exit', function () {
console.log();
process.stdout.write('Press any key to restart... or q to quit: ');
function keypress(key, e) {
if(e.ctrl) {
// allow ctr+c, z, etc
console.log();
} else if(key == 'q') {
console.log();
process.exit();
} else {
monitor.start(true);
}
process.stdin.pause();
process.stdin.setRawMode(false);
}
process.stdin.once('keypress', keypress);
process.stdin.setRawMode(true);
process.stdin.resume();
});
return server;
}

View File

@@ -1,3 +1,5 @@
/*globals ko:false, $:false, _:false, ui:false, Context:false, dpd:false, CollectionUtil:false */
(function() {
//var undoBtn = require('../view/undo-button-view');
@@ -82,7 +84,7 @@ function createPropertyViewModel(data, contextToAdd) {
self.editingName = ko.observable(self.name()).extend({variableName: true});
self.nameFocus = ko.observable();
self.isNew = contextToAdd !== null;
self.isNew = contextToAdd !== undefined;
self.tooltipEvent = new ui.Emitter();

View File

@@ -38,7 +38,7 @@ Files.prototype.handle = function (ctx, next) {
ctx.res.statusCode = 404;
respond('Resource Not Found', ctx.req, ctx.res);
})
.pipe(ctx.res)
.pipe(ctx.res);
};
module.exports = Files;

View File

@@ -25,15 +25,15 @@ function UserCollection(name, options) {
var config = this.config;
if(!config.properties) {
config.properties = {};
if(!this.properties) {
this.properties = {};
}
// username and password are required
config.properties.username = config.properties.username || {type: 'string'};
config.properties.username.required = true;
config.properties.password = config.properties.password || {type: 'string'};
config.properties.password.required = true;
this.properties.username = this.properties.username || {type: 'string'};
this.properties.username.required = true;
this.properties.password = this.properties.password || {type: 'string'};
this.properties.password.required = true;
}
util.inherits(UserCollection, Collection);

View File

@@ -133,7 +133,7 @@ function Server(options) {
});
server.on('request:error', function (err, req, res) {
console.log();
console.error();
console.error(req.method, req.url, err.stack || err);
process.exit();
});

18
lib/start.js Normal file
View File

@@ -0,0 +1,18 @@
var Server = require('./server')
, upgrade = require('doh').upgrade
, Monitor = require('./monitor')
, commands = {};
/**
* Commands exposed to parent process.
*/
commands.start = function (config, fn) {
var server = new Server(config);
upgrade(server);
server.on('listening', fn);
server.on('error', fn);
server.listen();
};
Monitor.createCommands(commands);

View File

@@ -45,6 +45,7 @@ module.exports = function loadTypes(basepath, fn) {
console.error();
console.error("Error loading module node_modules/" + file);
console.error(e.stack || e);
process.send({moduleError: e || true});
process.exit();
}
@@ -57,6 +58,8 @@ module.exports = function loadTypes(basepath, fn) {
d.on('error', function (err) {
console.error('Error in module node_modules/' + file);
console.error(err.stack || err);
process.send({moduleError: err});
d.dispose();
process.exit();
});
}

View File

@@ -1,14 +1,15 @@
{
"author": "Ritchie Martori",
"name": "deployd",
"version": "0.6.3",
"version": "0.6.5",
"description": "the simplest way to build realtime APIs for web and mobile apps",
"repository": {
"url": "git://github.com/deployd/deployd.git"
},
"engines": {
"node": ">= 0.6.0"
"node": ">= 0.8.0"
},
"main":"index",
"main": "index",
"dependencies": {
"validation": "*",
"mongodb": "1.0.2",
@@ -19,7 +20,7 @@
"filed": ">= 0.0.6",
"mkdirp": "*",
"wrench": "1.3.x",
"debug": "*",
"debug": ">= 0.7.0",
"scrubber": "*",
"shelljs": "https://github.com/dallonf/shelljs/tarball/master",
"http-proxy": "0.8.1",
@@ -31,7 +32,9 @@
"ejs": "0.7.x",
"async": "0.1.x",
"doh": ">=0.0.4",
"step": ">=0.0.5"
"step": ">=0.0.5",
"forever-monitor": "1.1.x",
"keypress": "~0.1.0"
},
"devDependencies": {
"mocha": "*",
@@ -40,8 +43,10 @@
"dox": "*",
"less": "*",
"jshint": "*"
},
"bin": { "dpd": "./bin/dpd" },
},
"bin": {
"dpd": "./bin/dpd"
},
"scripts": {
"test": "mocha",
"docs": "node docs/src/build.js"

View File

@@ -146,25 +146,52 @@ describe('User Collection', function() {
});
});
});
afterEach(function (done) {
this.timeout(10000);
dpd.users.logout(function () {
dpd.users.get(function (users) {
var total = users.length;
if(total === 0) return done();
users.forEach(function(user) {
dpd.users.del({id: user.id}, function () {
total--;
if(!total) {
done();
}
});
});
});
});
});
});
afterEach(function (done) {
this.timeout(10000);
dpd.users.logout(function () {
dpd.users.get(function (users) {
var total = users.length;
if(total === 0) return done();
users.forEach(function(user) {
dpd.users.del({id: user.id}, function () {
total--;
if(!total) {
done();
}
});
});
});
});
});
describe('dpd.emptyusers', function() {
describe('.post()', function() {
it('should store a username', function(done) {
chain(function(next) {
dpd.emptyusers.post({username: "hello", password: "password"}, next);
}).chain(function(next, res, err) {
if (err) return done(err);
expect(res).to.exist;
expect(res.username).to.equal("hello");
dpd.emptyusers.get(res.id, next);
}).chain(function(next, res, err) {
if (err) return done(err);
expect(res).to.exist;
expect(res.username).to.equal("hello");
done();
});
});
});
afterEach(function(done) {
dpd.emptyusers.logout(function() {
cleanCollection(dpd.emptyusers, function() {
done();
});
});
});
});
});

View File

@@ -0,0 +1,3 @@
{
"type": "UserCollection"
}

View File

@@ -53,7 +53,7 @@ describe('config-loader', function() {
});
it('should add internal resources', function(done) {
sh.mkdir('-p', path.join(basepath, 'resources/foo'));
sh.mkdir('-p', path.join(basepath, 'resources'));
configLoader.loadConfig(basepath, {}, function(err, resourceList) {
if (err) return done(err);
@@ -67,5 +67,25 @@ describe('config-loader', function() {
done(err);
});
});
it('should not attempt to load files', function(done) {
sh.mkdir('-p', path.join(basepath, 'resources'));
('').to(path.join(basepath, 'resources/.DS_STORE'));
configLoader.loadConfig(basepath, {}, function(err, resourceList) {
if (err) return done(err);
done();
});
});
it('should throw a sane error when looking for config.json', function(done) {
sh.mkdir('-p', path.join(basepath, 'resources/foo'));
configLoader.loadConfig(basepath, {}, function(err, resourceList) {
expect(err).to.exist;
expect(err.message).to.equal("Expected file: " + path.join('resources', 'foo', 'config.json'));
done();
});
});
});
});

View File

@@ -1,59 +1,69 @@
var Keys = require('../lib/keys');
var Keys = require('../lib/keys')
, KEY_FILE = __dirname + '/support/keys.json'
, fs = require('fs');
describe('Keys', function() {
describe('.get(key, callback)', function() {
it('should return a key if it exists', function(done) {
var keys = new Keys(__dirname + '/support/keys.json');
before(function () {
fs.writeFileSync(KEY_FILE, JSON.stringify({abcdefghijklmnopqrstuvwxyz: true}));
});
after(function () {
sh.rm(KEY_FILE);
});
keys.get('abcdefghijklmnopqrstuvwxyz', function(err, exists) {
expect(exists).to.equal(true);
done(err);
});
});
describe('.get(key, callback)', function() {
it('should return a key if it exists', function(done) {
var keys = new Keys(KEY_FILE);
it('should not throw if the file does not exist', function(done) {
var keys = new Keys(__dirname + '/support/file-doesnt-exist.json');
keys.get('abcdefghijklmnopqrstuvwxyz', function(err, exists) {
expect(exists).to.equal(true);
done(err);
});
});
keys.get('abcdefghijklmnopqrstuvwxyz', function(err, exists) {
expect(exists).to.equal(undefined);
done(err);
});
});
});
it('should not throw if the file does not exist', function(done) {
var keys = new Keys(__dirname + '/support/file-doesnt-exist.json');
describe('.generate()', function() {
it('should create a new key', function() {
var keys = new Keys()
, key = keys.generate();
keys.get('abcdefghijklmnopqrstuvwxyz', function(err, exists) {
expect(exists).to.equal(undefined);
done(err);
});
});
});
expect(key).to.exist;
expect(key.length).to.equal(512);
});
});
describe('.generate()', function() {
it('should create a new key', function() {
var keys = new Keys()
, key = keys.generate();
describe('.create(callback)', function() {
it('create a new key which should then exist', function(done) {
var keys = new Keys(__dirname + '/support/keys.json');
expect(key).to.exist;
expect(key.length).to.equal(512);
});
});
keys.create(function(err, key) {
expect(err).to.not.exist;
keys.get(key, function(err, exists) {
expect(exists).to.equal(true);
done(err);
});
});
});
});
describe('.create(callback)', function() {
it('create a new key which should then exist', function(done) {
var keys = new Keys(__dirname + '/support/keys.json');
describe('.getLocal(fn)', function() {
it('should get the first local key', function(done) {
var keys = new Keys(__dirname + '/support/keys.json');
keys.getLocal(function(err, key) {
expect(key).to.exist;
expect(key.length).to.equal(26);
done(err);
});
});
});
keys.create(function(err, key) {
expect(err).to.not.exist;
keys.get(key, function(err, exists) {
expect(exists).to.equal(true);
done(err);
});
});
});
});
describe('.getLocal(fn)', function() {
it('should get the first local key', function(done) {
var keys = new Keys(__dirname + '/support/keys.json');
keys.getLocal(function(err, key) {
expect(key).to.exist;
expect(key.length).to.equal(26);
done(err);
});
});
});
});

32
test/monitor.unit.js Normal file
View File

@@ -0,0 +1,32 @@
var Monitor = require('../lib/monitor');
describe('Monitor', function(){
describe('Dynamic Commands', function(){
it('should execute a command on the child process', function(done) {
var monitor = new Monitor(__dirname + '/support/sample-start.js', {silent: true});
monitor.start(function (err, commands) {
commands.test('hello world', function (err, msg) {
expect(msg).to.equal('hello world');
done();
})
})
})
})
it('should restart processes after they crash', function(done) {
this.timeout(10000);
var monitor = new Monitor(__dirname + '/support/sample-start.js', {silent: true});
monitor.start(function (err, commands, restarting) {
if(restarting) {
setTimeout(function () {
commands.test('hello world', function (err, msg) {
expect(msg).to.equal('hello world');
done();
});
}, 100);
} else {
commands.crash();
}
})
})
})

View File

@@ -27,7 +27,7 @@ function fauxRes() {
function fauxServer() {
return {
emit: function () {}
}
};
}
describe('Router', function() {
@@ -96,7 +96,7 @@ describe('Router', function() {
res.end = function () {
if(res.statusCode != 404) throw new Error('incorrect status for resource not found');
done();
}
};
foo.handle = function() {
throw "/foo was handled";
@@ -126,7 +126,7 @@ describe('Router', function() {
res.end = function () {
if(res.statusCode != 404) throw new Error('incorrect status for resource not found');
done();
}
};
router.route(req, res);
});

View File

@@ -1,10 +1,17 @@
var Server = require('../lib/server')
, Db = require('../lib/db').Db
, Store = require('../lib/db').Store
, Router = require('../lib/router');
, Router = require('../lib/router')
, sh = require('shelljs');
describe('Server', function() {
describe('.listen()', function() {
beforeEach(function() {
sh.cd('./test/support/proj');
sh.rm('-rf', 'resources');
sh.mkdir('resources');
});
it('should start a new deployd server', function(done) {
var PORT = genPort();
var opts = {
@@ -16,8 +23,9 @@ describe('Server', function() {
}
};
var server = new Server(opts);
server.listen();
expect(server.db instanceof Db).to.equal(true);
expect(server.options).to.eql(opts);
server.on('listening', function () {
@@ -25,6 +33,10 @@ describe('Server', function() {
done();
});
});
afterEach(function() {
sh.cd('../../../');
});
});
describe('.createStore(namespace)', function() {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
var Monitor = require('../../lib/monitor');
var commands = {};
commands.test = function (msg, fn) {
fn(null, msg);
}
commands.crash = function () {
throw 'crash!';
}
Monitor.createCommands(commands);