mirror of
https://github.com/zhigang1992/deployd.git
synced 2026-04-28 17:35:50 +08:00
merge commit
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
language: node_js
|
||||
services: mongodb
|
||||
node_js:
|
||||
- 0.6
|
||||
- 0.8
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- master
|
||||
|
||||
|
||||
12
HISTORY.md
12
HISTORY.md
@@ -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
|
||||
|
||||
|
||||
14
README.md
14
README.md
@@ -1,10 +1,10 @@
|
||||
# deployd v0.6.2
|
||||
# deployd v0.6.4
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
2
bin/dpd
2
bin/dpd
@@ -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
11
docs/about.md
Normal 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
23
docs/deploy.md
Normal 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
32
docs/examples.md
Normal 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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
91
docs/index.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
## 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.'
|
||||
|
||||

|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
77
docs/reference/advanced-queries.md
Normal file
77
docs/reference/advanced-queries.md
Normal 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
|
||||
}
|
||||
168
docs/reference/collection-events.md
Normal file
168
docs/reference/collection-events.md
Normal 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
91
docs/reference/dpdjs.md
Normal 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)`.
|
||||
|
||||
58
docs/reference/modifiers.md
Normal file
58
docs/reference/modifiers.md
Normal 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*
|
||||
185
docs/resources/collection.md
Normal file
185
docs/resources/collection.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Collection
|
||||
|
||||
The Collection resource allows clients to save, update, delete, and query data of a given type.
|
||||
|
||||
## Properties
|
||||
|
||||

|
||||
|
||||
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
|
||||
});
|
||||
213
docs/resources/custom-resources.md
Normal file
213
docs/resources/custom-resources.md
Normal 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'
|
||||
}]
|
||||
};
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
});
|
||||
63
docs/resources/user-collection.md
Normal file
63
docs/resources/user-collection.md
Normal 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
|
||||
});
|
||||
205
docs/tutorials/comments-1.md
Normal file
205
docs/tutorials/comments-1.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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>
|
||||
99
docs/tutorials/hello-world.md
Normal file
99
docs/tutorials/hello-world.md
Normal 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`).
|
||||
|
||||

|
||||
|
||||
You have just added your first resource to Deployd!
|
||||
|
||||
## Collections
|
||||
|
||||
The dashboard should open up the Collection Property editor.
|
||||
|
||||

|
||||
|
||||
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>
|
||||
11
index.js
11
index.js
@@ -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;
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
157
lib/monitor.js
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
18
lib/start.js
Normal 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);
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
19
package.json
19
package.json
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
3
test-app/resources/empty-users/config.json
Normal file
3
test-app/resources/empty-users/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "UserCollection"
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
32
test/monitor.unit.js
Normal 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();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
14
test/support/sample-start.js
Normal file
14
test/support/sample-start.js
Normal 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user