|
| 1 | +# ACP |
| 2 | + |
| 3 | +[](https://travis-ci.org/samt/acpjs) |
| 4 | + |
| 5 | +Data-agnostic admin interface generator for CRUD applications |
| 6 | + |
| 7 | +## Abstract |
| 8 | + |
| 9 | +The problem with crud apps is they are all are basically the same, yet we find |
| 10 | +it necessary to always rewrite the same code over and over. We have a set of |
| 11 | +requirements that remain relatively constant: |
| 12 | + |
| 13 | +- Views Types |
| 14 | + - Summary (Dashboard) view of data for graphing or general display |
| 15 | + - Unordered List-view (or table) of items in a collection |
| 16 | + - Ordred list-view (or table) of items in a collection for sorting |
| 17 | + - Log views (uneditable display of entries) |
| 18 | + - Insert/update Item view within a collection |
| 19 | +- Functionality |
| 20 | + - View all collections in appropriate view types |
| 21 | + - Validation of data prior to database |
| 22 | + - Deny badly formed data (email addresses must have an "@" sign) |
| 23 | + - Deny duplicate entries (i.e. duplicate username fields) |
| 24 | + - Deny baed upon custom, applicaiton-specific criteria |
| 25 | + - Pagination |
| 26 | + - Filter/Search |
| 27 | + - Form controls in editing views |
| 28 | + |
| 29 | +In addition, we always have data coming in from somewhere. Sometimes it's all |
| 30 | +from a database, sometimes it's from flat files, and even sometimes it's from |
| 31 | +the network. The problem with other frameworks is they are all dependent on one |
| 32 | +type of data retrieval. If you try to exit the box they put you in, you're |
| 33 | +going to have a bad time. |
| 34 | + |
| 35 | +## Requirements |
| 36 | + |
| 37 | +- node >= 0.8.0 |
| 38 | +- express >= 4.x.x (if using with existing app) |
| 39 | + |
| 40 | +## Installation |
| 41 | + |
| 42 | +``` |
| 43 | +npm install acp |
| 44 | +``` |
| 45 | + |
| 46 | +## Usage |
| 47 | + |
| 48 | +### Basic |
| 49 | + |
| 50 | +```javascript |
| 51 | +var acp = require('acp'); |
| 52 | +var adm = acp(); |
| 53 | + |
| 54 | +adm.listen(3000); // identical to express's app.listen() |
| 55 | +``` |
| 56 | + |
| 57 | +### With an existing express app |
| 58 | + |
| 59 | +Because acp is an express application, we can simply mount it on top of another |
| 60 | +application currently being set up. This allows you to write your own custom |
| 61 | +user-facing front-end and allow you to use acp to mange your data behind. |
| 62 | + |
| 63 | +```javascript |
| 64 | +var adm = acp(); |
| 65 | +var app = express(); |
| 66 | + |
| 67 | +// ... |
| 68 | + |
| 69 | +app.use('/admin', adm); // mount on "admin" |
| 70 | + |
| 71 | +app.listen(3000); // do not listen with adm |
| 72 | +``` |
| 73 | + |
| 74 | +### Define Collections |
| 75 | + |
| 76 | +A collection is any set of data that you wish to manage. You can pull it from |
| 77 | +your database, flat files, remote systems, etc. |
| 78 | + |
| 79 | +```javascript |
| 80 | +var acp = require('acp'); |
| 81 | +var adm = acp(); |
| 82 | + |
| 83 | +var userCollection = adm.define('Users', { |
| 84 | + primaryKey: 'id', // defaults to 'id' |
| 85 | + mount: 'auto', // defaults to 'auto. Mounts CRUD pages on url-safe version of the collection name |
| 86 | + create: createUser,// function(record, cb) , cb(err, record), |
| 87 | + readOne: getOneUser,// function ( primaryKey ), |
| 88 | + read: getUsers, // function( { start: 0, limit: 10 }), |
| 89 | + update: updateUser, // function ( primaryKey, record ), |
| 90 | + delete: deleteUser, // function ( primaryKey | record ), cb(Error err, bool deleted) |
| 91 | + count: countUsers, // function (cb), cb(Error err, number count), |
| 92 | + fields: { |
| 93 | + id: { type: 'auto', primary: true }, // auto_int sets nice defaults |
| 94 | + slug: { type: 'string', filter: [ ACP.Filter.urlSafe, }, |
| 95 | + name: { type: 'string', validate: function (n) { |
| 96 | + return /[a-z0-9_\-]{4,10}/i.test(n); |
| 97 | + }}, |
| 98 | + email: { type: 'string', validate: [ ACP.Validate.email, function (email, next) { // validate can take an array |
| 99 | + db.query('SELECT * FROM users WHERE email = ?', [ email ], function (err, rows) { |
| 100 | + if (err) throw err; |
| 101 | + next( !rows.length ); |
| 102 | + }); |
| 103 | + }]} |
| 104 | + } |
| 105 | +}); |
| 106 | + |
| 107 | +adm.listen(3000); |
| 108 | +``` |
| 109 | + |
| 110 | +### Arbitrary Page |
| 111 | + |
| 112 | +```javascript |
| 113 | +var acp = require('acp'); |
| 114 | +var admin = acp(); |
| 115 | + |
| 116 | +// same arguments as .define() |
| 117 | +admin.page('Dashboard', { |
| 118 | + mount: '/', |
| 119 | + widgets: [ // table of widgets |
| 120 | + [w1, w2, w3], |
| 121 | + [w4, w5] |
| 122 | + ] |
| 123 | +}); |
| 124 | +``` |
| 125 | + |
| 126 | +## API |
| 127 | + |
| 128 | +### Class `ACP` |
| 129 | + |
| 130 | +This is the main class of the Admin Control Panel Interface. |
| 131 | + |
| 132 | +#### `ACP.define(name, options)` |
| 133 | + |
| 134 | +Defines a collection of data to keep track of |
| 135 | + |
| 136 | +- `name` String: Name of the collection, usually pular |
| 137 | +- `options` Object: |
| 138 | + - `primaryKey` String: Field name of the primary key. |
| 139 | + - `disableCrud` Boolean: (optional) Disable crud functionality? default: |
| 140 | + false |
| 141 | + - `mount` String: (optional) 'auto' to mount on root with generated slug |
| 142 | + (default), '/routname' otherwise. |
| 143 | + - `orderedBy` String: (optional) Field name to order by if ordered list. |
| 144 | + false or null otherwise. Default: false |
| 145 | + - `create` Function: callback to store a new record. |
| 146 | + Params: `record`, `callback(err, record`) |
| 147 | + - `update` Function: callback to update a record. |
| 148 | + Params: `record`, `callback (err, record)` |
| 149 | + - `readOne` Function: callback to read one record. |
| 150 | + Params: `primaryKey`, `callback(err, record)` |
| 151 | + - `read` Function: callback to get the collection. |
| 152 | + Params: `filter` Array< _ACP.Filter_ >, `callback(err, recs)` |
| 153 | + - `delete` Function callback to delete a record. |
| 154 | + Params: `primaryKey`, `callback(err, deleted?)` |
| 155 | + - `count` Function callback to count all records. |
| 156 | + Params: `Array<ACP.Filter>, callback(err, count)` |
| 157 | + - `fields` Array<ACP.Field>: Field List |
| 158 | + |
| 159 | +#### `ACP.page(name, options)` |
| 160 | + |
| 161 | +- `name` String: Name of the page |
| 162 | +- `options` Object: |
| 163 | + - `mount` String: (optional) 'auto' to mount on root with generated slug |
| 164 | + (default), '/routname' otherwise. |
| 165 | + - `widgets` Array< Array<ACP.Widget> >: Widget table. Outer Array: Rows; |
| 166 | + inner Array: Columns |
| 167 | + |
| 168 | +### Class `ACP.Filter` |
| 169 | + |
| 170 | +Plain object. |
| 171 | + |
| 172 | +- `field_name` Object: |
| 173 | + - `eq` Mixed: Returned values must be equal. |
| 174 | + - `ne` Mixed: Returned values must not be equal |
| 175 | + - `gt` Mixed: Returned values must be greater than |
| 176 | + - `lt` Mixed: Returned values must be less than |
| 177 | + - `gte` Mixed: Returned values must be greater than or equals |
| 178 | + - `lte` Mixed: Returns values must be less than or equals |
| 179 | + |
| 180 | +For example: |
| 181 | + |
| 182 | +```javascript |
| 183 | +{ email : { ne : '[email protected] '} } |
| 184 | +``` |
| 185 | + |
| 186 | +### Class `ACP.Field` |
| 187 | + |
| 188 | +Is just a plain object with the following properties: |
| 189 | + |
| 190 | +- `type` String: 'auto', 'number', 'string', 'text', 'bool', 'datetime', 'date' |
| 191 | +- `primary` Boolean: Is primary key? Default: false. Overridden if primaryKey is set in collection |
| 192 | +- `editable` Boolean: Can the user edit this field? |
| 193 | +- `filter` Function|Array<Function>: Filter (modify values) of the record |
| 194 | +- `validate` Function|Array<Function>: Validate a field. performed AFTER filters |
| 195 | +- `options` Array<String>|Plain Object: (Optional) if this is a dropdown/radio/checkbox list, any |
| 196 | +- `input` Flag: ACP.TEXT, ACP.PASSWORD, ACP.EMAIL, ACP.DROPDOWN, |
| 197 | + ACP.DROPDOWN_MULI, ACP.DATE_PICKER, ACP.DATETIME_PICKER, ACP.RADIO, ACP.CHECKBOX |
| 198 | + |
| 199 | +### Class `ACP.Widget` |
| 200 | + |
| 201 | +- `size` Number: 1-12, grid width. Default: 3 |
| 202 | +- `template` String: Either a built in template or user-defined custom path |
| 203 | +- `read` Function: Callback to read data to populate the widget width |
| 204 | +- `title` String: Widget title |
| 205 | + |
| 206 | +### Class `ACP.Filter` |
| 207 | + |
| 208 | +Contains the functions to validate automatically |
| 209 | + |
| 210 | +- `ACP.Filter.urlSafe`: Makes a URL safe string (RFC 3986) |
| 211 | +- `ACP.Filter.slug`: Makes the string into a clean URL-safe sting [a-z0-9\-_] |
| 212 | +- `ACP.Filter.boolean`: Converts strings from <form>s "1" or "0" into true boolean values |
| 213 | +- `ACP.Filter.datetime`: Parses the date to an ISO 8601 compliant string |
| 214 | +- `ACP.Filter.jsDate`: Turns field into a JavaScript `Date` object |
| 215 | + |
| 216 | +### Class `ACP.Validate` |
| 217 | + |
| 218 | +Contains the functions to validate automatically |
| 219 | + |
| 220 | +- `ACP.Validate.email`: Validates an email |
| 221 | +- `ACP.Validate.url`: Validates a URL |
| 222 | +- `ACP.Validate.foreignKey(collectionName)`: Validates that there exists a primary key in the collection |
| 223 | +- `ACP.Validate.unique`: Validates that there are no records with the same value in this field |
| 224 | + |
| 225 | +## Concepts |
| 226 | + |
| 227 | +### Data Agnostic |
| 228 | + |
| 229 | +When you create a collection, you specify where the data comes from, how it is |
| 230 | +inserted, how it is updated, and what validations need to take place. There are |
| 231 | +validation macros you can use for extremely common validations, such as email |
| 232 | +addresses, url-safe strings, and checking for duplicates in other models. You |
| 233 | +may also specify filters which take data pre-validation and operate on it. |
| 234 | + |
| 235 | +### Reusable interfaces |
| 236 | + |
| 237 | +You don't have to write your own views. You specify where and when you |
| 238 | +want things to appear and it just makes it for you. |
| 239 | + |
| 240 | +### Built with express |
| 241 | + |
| 242 | +To make hooking it into your stack easier, it is made to either piggy-back off |
| 243 | +of your current express application or you can specify to run it standalone. |
| 244 | + |
| 245 | +### Security |
| 246 | + |
| 247 | +Because this is going to give a user a one-stop-shop for mangling the database, |
| 248 | +we wanted to ensure only the people you give access will have access. There are |
| 249 | +few layers of security we've added on: |
| 250 | + |
| 251 | +- Specify what session data must be set before access |
| 252 | +- Optionally enable "reauth" if the user hasn't been active within a |
| 253 | + specified amount of time |
| 254 | +- Session keys in the URL as query vars for additional session validation |
| 255 | +- Referrer checking |
| 256 | + |
| 257 | +## License |
| 258 | + |
| 259 | +The MIT License |
0 commit comments