waxx 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/LICENSE +201 -0
- data/README.md +879 -0
- data/bin/waxx +120 -0
- data/lib/waxx/app.rb +173 -0
- data/lib/waxx/conf.rb +54 -0
- data/lib/waxx/console.rb +204 -0
- data/lib/waxx/csrf.rb +14 -0
- data/lib/waxx/database.rb +80 -0
- data/lib/waxx/encrypt.rb +38 -0
- data/lib/waxx/error.rb +60 -0
- data/lib/waxx/html.rb +33 -0
- data/lib/waxx/http.rb +268 -0
- data/lib/waxx/init.rb +273 -0
- data/lib/waxx/irb.rb +44 -0
- data/lib/waxx/irb_env.rb +18 -0
- data/lib/waxx/json.rb +23 -0
- data/lib/waxx/mongodb.rb +221 -0
- data/lib/waxx/mysql2.rb +234 -0
- data/lib/waxx/object.rb +115 -0
- data/lib/waxx/patch.rb +138 -0
- data/lib/waxx/pdf.rb +69 -0
- data/lib/waxx/pg.rb +246 -0
- data/lib/waxx/process.rb +270 -0
- data/lib/waxx/req.rb +116 -0
- data/lib/waxx/res.rb +98 -0
- data/lib/waxx/server.rb +304 -0
- data/lib/waxx/sqlite3.rb +237 -0
- data/lib/waxx/supervisor.rb +47 -0
- data/lib/waxx/test.rb +162 -0
- data/lib/waxx/util.rb +57 -0
- data/lib/waxx/version.rb +3 -0
- data/lib/waxx/view.rb +389 -0
- data/lib/waxx/waxx.rb +73 -0
- data/lib/waxx/x.rb +103 -0
- data/lib/waxx.rb +50 -0
- data/skel/README.md +11 -0
- data/skel/app/app/app.rb +39 -0
- data/skel/app/app/error/app_error.rb +16 -0
- data/skel/app/app/error/dhtml.rb +9 -0
- data/skel/app/app/error/html.rb +8 -0
- data/skel/app/app/error/json.rb +8 -0
- data/skel/app/app/error/pdf.rb +13 -0
- data/skel/app/app/log/app_log.rb +13 -0
- data/skel/app/app.rb +20 -0
- data/skel/app/home/home.rb +16 -0
- data/skel/app/home/html.rb +145 -0
- data/skel/app/html.rb +192 -0
- data/skel/app/usr/email.rb +66 -0
- data/skel/app/usr/html.rb +115 -0
- data/skel/app/usr/list.rb +51 -0
- data/skel/app/usr/password.rb +54 -0
- data/skel/app/usr/record.rb +98 -0
- data/skel/app/usr/usr.js +67 -0
- data/skel/app/usr/usr.rb +277 -0
- data/skel/app/waxx/waxx.rb +109 -0
- data/skel/bin/README.md +1 -0
- data/skel/db/README.md +11 -0
- data/skel/db/app/0-init.sql +88 -0
- data/skel/lib/README.md +1 -0
- data/skel/log/README.md +1 -0
- data/skel/opt/dev/config.yaml +1 -0
- data/skel/opt/prod/config.yaml +1 -0
- data/skel/opt/stage/config.yaml +1 -0
- data/skel/opt/test/config.yaml +1 -0
- data/skel/private/README.md +1 -0
- data/skel/public/lib/site.css +202 -0
- data/skel/public/lib/waxx/w.ico +0 -0
- data/skel/public/lib/waxx/w.png +0 -0
- data/skel/public/lib/waxx/waxx.js +111 -0
- data/skel/tmp/pids/README.md +1 -0
- data.tar.gz.sig +0 -0
- metadata +140 -0
- metadata.gz.sig +3 -0
data/README.md
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
# Waxx - Web Application X(x)
|
|
2
|
+
|
|
3
|
+
**NOTICE: This is the first public release of Waxx and the APIs may change. Do not build large production apps with it yet!**
|
|
4
|
+
|
|
5
|
+
**NOTICE: Waxx does not run on Windows yet. Working on it. Stay tuned.**
|
|
6
|
+
|
|
7
|
+
The Waxx Framerwork is a high perfomance, functional-inspired (but not truly functional), web application development environment written in Ruby and inspired by Go and Haskel.
|
|
8
|
+
|
|
9
|
+
## Goals
|
|
10
|
+
|
|
11
|
+
1. High Perfomance (similar to Node and Go)
|
|
12
|
+
2. Easy to grok
|
|
13
|
+
3. Fast to develop
|
|
14
|
+
4. Efficient to maintain
|
|
15
|
+
5. Fast and easy to deploy
|
|
16
|
+
|
|
17
|
+
## Target Users
|
|
18
|
+
|
|
19
|
+
The Waxx Framework was developed to build CRUD applications and REST and RPC services. It scales very well on multi-core machines and is [will be] suitable for very large deployments.
|
|
20
|
+
|
|
21
|
+
## Who's Behind This
|
|
22
|
+
|
|
23
|
+
Waxx was developed by Dan Fitzpatrick at [ePark Labs](https://www.eparklabs.com/).
|
|
24
|
+
|
|
25
|
+
## Hello World
|
|
26
|
+
|
|
27
|
+
_app/hello/hello.rb:_
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
module App::Hello
|
|
31
|
+
extend Waxx::Object
|
|
32
|
+
|
|
33
|
+
runs(
|
|
34
|
+
default: "world",
|
|
35
|
+
world: {
|
|
36
|
+
desc: "This says Hello World",
|
|
37
|
+
get: -> (x) {
|
|
38
|
+
x << "Hello World!"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
URL: example.com/hello or example.com/hello/world
|
|
46
|
+
|
|
47
|
+
returns "Hello World"
|
|
48
|
+
|
|
49
|
+
NOTE: This is not the way you build a normal app. Just here because everyone wants to see a Hello World example.
|
|
50
|
+
|
|
51
|
+
## Introduction to Waxx
|
|
52
|
+
|
|
53
|
+
### Normal Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
sudo gem install waxx
|
|
57
|
+
waxx init site
|
|
58
|
+
cd site
|
|
59
|
+
waxx on
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Secure Install
|
|
63
|
+
|
|
64
|
+
The Waxx gem is cryptographically signed to be sure the gem you install hasn't been tampered with.
|
|
65
|
+
Because of this you need to add the public key to your list of trusted gem certs.
|
|
66
|
+
Follow theese direction. (You only need step one the first time you install the gem.)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
sudo gem cert --add <(curl -s https://www.waxx.io/waxx-gem-public-key.pem)
|
|
70
|
+
sudo gem install waxx -P HighSecurity
|
|
71
|
+
waxx init site
|
|
72
|
+
cd site
|
|
73
|
+
waxx on
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Visit [http://localhost:7777/](http://localhost:7777/). If you want a different port, edit `opt/dev/config.yaml` first.
|
|
77
|
+
Then run `waxx buff` (waxx off && waxx on) or if you prefer `waxx restart`
|
|
78
|
+
|
|
79
|
+
See [Install Waxx](https://www.waxx.io/doc/install) for complete details.
|
|
80
|
+
|
|
81
|
+
### High Performance
|
|
82
|
+
Waxx is multi-threaded queue-based system.
|
|
83
|
+
You specify the number of threads in the config file.
|
|
84
|
+
Each thread is prespawned and each thread makes it's own database connection.
|
|
85
|
+
Requests are received and put into a FIFO request queue.
|
|
86
|
+
The threads work through the queue.
|
|
87
|
+
Each request, including session management, a database query, access control, and rendering in HTML or JSON is approximately 1-2ms (on a modern Xeon server).
|
|
88
|
+
With additional libraries, Waxx also easily generates XML, XLSX, CSV, PDF, etc.
|
|
89
|
+
|
|
90
|
+
### Easy to Grok
|
|
91
|
+
Waxx has no Classes.
|
|
92
|
+
It is Module-based and the Modules have methods (functions).
|
|
93
|
+
Each method within a Module is given parameters and the method runs in isolation.
|
|
94
|
+
There are no instance variables and no global variables.
|
|
95
|
+
Consequently, it is very easy to understand what any method does and it is very easy to test methods.
|
|
96
|
+
You can call any method in the whole system from the console using `waxx console`.
|
|
97
|
+
Passing in the same variables to a function will always return the same result.
|
|
98
|
+
Waxx does have `x.res.out` variable, which is appended to with `x << "text"`, that is passed into each method and any method can append to the response body or set response headers.
|
|
99
|
+
So it is not truly functional because this is considered a side effect.
|
|
100
|
+
My opinion is that when you are building a response, then copying the response on every method is a waste of resources.
|
|
101
|
+
So it does have this side effect by design.
|
|
102
|
+
|
|
103
|
+
#### Waxx Terminology
|
|
104
|
+
|
|
105
|
+
- Object: A database table or database object or a container for some specific functionality
|
|
106
|
+
- Object `has` fields (an array of Hashes). A field is both a database field/column/attribute and a UI control (for HTML apps)
|
|
107
|
+
- Field `is` (represents) a single object (INNER JOIN) or many related objects (LEFT JOIN)
|
|
108
|
+
- Object `runs` a URL path - business logic (normally get from or post to a view)
|
|
109
|
+
- View: Like a DB view -- fields from one or more tables/objects
|
|
110
|
+
- Html, Json, Xlsx, Tab, Csv, Pdf, etc.: How to render the view
|
|
111
|
+
- x is a variable that is passed to nearly all methods and contains the request (`x.req` contains: get and post vars, request cookies, and environment), response (`x.res` contains the status, response cookies, headers, and content body), user session (x.usr), and user agent (x.ua) cookies
|
|
112
|
+
|
|
113
|
+
#### The "x" Variable
|
|
114
|
+
|
|
115
|
+
- `x.req` contains: get and post vars, request cookies, environment, and some helper methods
|
|
116
|
+
- `x.req.get` is a hash of vars passed in the query string
|
|
117
|
+
- `x.req.post` is a hash of vars passed in the body of the request
|
|
118
|
+
- `x.req.env` is a hash of the environment
|
|
119
|
+
- `x.req['Header-Name']` is a shortcut to incoming headers / environment vars
|
|
120
|
+
- `x['param_name']` and `x/:param_name` are shortcuts to get and post vars (post vars override get vars)
|
|
121
|
+
- `x.res` contains the status, response cookies, headers, and content body
|
|
122
|
+
- `x << "some text"` appends to the body
|
|
123
|
+
- `x.res['Header-Name'] = "value"` to set a response header
|
|
124
|
+
- `x.status = 404` set the status. Defaults to 200.
|
|
125
|
+
- `x.usr` is the session cookie hash (set expiration params in opt/{env}/config.yaml)
|
|
126
|
+
- `x.usr['name'] = 'Joe'` will set the name variable in the `x.usr` variable accross requests.
|
|
127
|
+
- `x.ua` is the client (user agent) cookie hash (set expiration params in opt/{env}/config.yaml). This is normally a long-lived cookie to store login name, remember me, last visit, etc.
|
|
128
|
+
- `x.ua['uname'] = 'jb123'` will set the name variable in the `x.ua` variable accross requests.
|
|
129
|
+
|
|
130
|
+
See [Waxx Docs](https://www.waxx.io/doc/code) for more info.
|
|
131
|
+
|
|
132
|
+
#### A request is processed as follows:
|
|
133
|
+
|
|
134
|
+
1. HTTP request is received by Waxx (Use a reverse proxy/load balancer/https server like NGINX first for production)
|
|
135
|
+
2. The request is placed in a queue: `Waxx::Server.queue`
|
|
136
|
+
3. The request is popped off the queue by a Ruby green thread and parsed
|
|
137
|
+
4. The variable `x` is created with the request `x.req` and response `x.res`.
|
|
138
|
+
5. The run method is called for the appropriate app (a namespaced RPC). All routes are: /app/act/[arg1/args2/arg3/...] => app is the module and act is the method to call with the args.
|
|
139
|
+
6. You output to the response using `x << "output"` or using helper methods: `App::Html.page(...)`
|
|
140
|
+
7. The response is returned to the client. Partial, chunked, and streamed responses are supported as well as you have direct access to the IO.
|
|
141
|
+
|
|
142
|
+
## Fast to Develop
|
|
143
|
+
|
|
144
|
+
Waxx was built with code maintainablity in mind. The following principles help in maintaining Waxx apps:
|
|
145
|
+
|
|
146
|
+
1. Simple to know where the code is located for any URI. A request to /person/list will start in the `app/person/person.rb` file and normally use the view defined in the file `app/person/list.rb`
|
|
147
|
+
2. Fields are defined upfront. The fields you want to use in your app are defined in the Object file `app/person/person.rb`
|
|
148
|
+
3. Field have attributes that make a lot of UI development simple (optional). has(email: {label: "Email Address" ...})`
|
|
149
|
+
4. Views allow you to see exactly what is on an interface and all the business logic. Only the fields on a view can be updated so it is impossible to taint the database by passing in extra parameters.
|
|
150
|
+
5. Most rendering is automatic unless you want to do special stuff. You can use pure Ruby functions or your favorite template engine. The View file `app/person/list.rb` contains all of the fields, joined tables, and layout for a view.
|
|
151
|
+
6. Full visibility into the external API and each endpoint's access control allows to immediate auditing of who can see and do what.
|
|
152
|
+
|
|
153
|
+
There are no routes.
|
|
154
|
+
All paths are `/:app/:act/:arg1/:arg2/...`.
|
|
155
|
+
The URL maps to an App and runs the act (method).
|
|
156
|
+
For example: `example.com/person/list` will execute the `list` method in the `App::Person` module.
|
|
157
|
+
This method is defined in `app/person/person.rb`.
|
|
158
|
+
Another example: A request to `/website_page/content/3` will execute the `content` method in the `App::WebsitePage` app and pass in `3` as the first parameter after 'x'.
|
|
159
|
+
There is a default app and a default method in each app.
|
|
160
|
+
So a request to `example.com/` will show the home page if the default app is `website` and the default method in website is `home`.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
### File Structure
|
|
164
|
+
Waxx places each module in it's own directory. This includes the Object, Runner, Views, Layouts, and Tests.
|
|
165
|
+
I normally place my app-specific javascript and css in this same folder as well.
|
|
166
|
+
In this way, all of the functionality and features of a specific App or Module are fully self-contained.
|
|
167
|
+
However, you can optionally put your files anywhere and require them in your code.
|
|
168
|
+
So if you like all the objects to be in one folder you can do that.
|
|
169
|
+
If you work with a large team where backend and frontend people do not overlap, then maybe that will work for you.
|
|
170
|
+
|
|
171
|
+
This is a normal structure:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
.
|
|
175
|
+
|-- app # Your apps go here. Also Waxx::App::Root
|
|
176
|
+
| |-- app.rb # Site-specific methods
|
|
177
|
+
| |-- html.rb # The shared HTML layout and helpers
|
|
178
|
+
| |-- app # Customizable waxx helper apps (logging and error handling)
|
|
179
|
+
| | |-- app.rb # App/generic functions
|
|
180
|
+
| | |-- error
|
|
181
|
+
| | | |-- app_error.rb # Error handler
|
|
182
|
+
| | | |-- dhtml.rb # Render a Dhtml error
|
|
183
|
+
| | | |-- html.rb # Render an Html error
|
|
184
|
+
| | | `-- json.rb # Render a Json error
|
|
185
|
+
| | |-- log
|
|
186
|
+
| | | `-- app_log.rb # Log to your chosen logging system
|
|
187
|
+
| |-- company # An app
|
|
188
|
+
| | |-- company.rb # An object and router for /company
|
|
189
|
+
| | `-- list.rb # A view (fields and layout)
|
|
190
|
+
| |-- person # The person app
|
|
191
|
+
| | |-- html.rb # Shared HTML for the person app
|
|
192
|
+
| | |-- person.rb # The Person object and /person methods
|
|
193
|
+
| | `-- profile.rb # The Person::Profile view
|
|
194
|
+
| |-- grp # Grp app (included in Waxx)
|
|
195
|
+
| | `-- grp.rb
|
|
196
|
+
| |-- usr # Usr app (included in Waxx)
|
|
197
|
+
| | |-- email.rb
|
|
198
|
+
| | |-- grp
|
|
199
|
+
| | |-- html.rb
|
|
200
|
+
| | |-- list.rb
|
|
201
|
+
| | |-- password.rb
|
|
202
|
+
| | |-- record.rb
|
|
203
|
+
| | |-- usr.js
|
|
204
|
+
| | `-- usr.rb
|
|
205
|
+
| `-- website # The website app (included in Waxx)
|
|
206
|
+
| |-- html.rb # Html for the website
|
|
207
|
+
| |-- page # website_page app
|
|
208
|
+
| | |-- list.rb # List webpages
|
|
209
|
+
| | |-- record.rb # Edit a webpage
|
|
210
|
+
| | `-- website_page.rb # WebsitePage object and methods
|
|
211
|
+
| `-- website.rb # Website Object and methods/routes
|
|
212
|
+
|-- bin
|
|
213
|
+
| `-- waxx # The waxx bin does everything (on off buff make test deploy etc)
|
|
214
|
+
|-- db # Store database stuff here
|
|
215
|
+
| `-- app # Migrations live in here (straight one-way SQL files). Each db has its own folder
|
|
216
|
+
| |-- 0-waxx.sql # The initial migration that adds support for migrations to the DB
|
|
217
|
+
| `-- 201612240719-invoice.sql # A migration YmdHM-name.sql (`waxx migration invoice` makes this)
|
|
218
|
+
|-- lib # The libraries used by your app (waxx is included)
|
|
219
|
+
|-- log # The log folder (optional)
|
|
220
|
+
| `-- waxx.log
|
|
221
|
+
|-- opt # Config for each environment
|
|
222
|
+
| |-- active -> dev # Symlink to the active environment
|
|
223
|
+
| |-- deploy.yaml # Defines how to deploy to each environment
|
|
224
|
+
| |-- dev # The dev environment
|
|
225
|
+
| | `-- config.yaml
|
|
226
|
+
| |-- stage # The stage environment
|
|
227
|
+
| | |-- config.yaml
|
|
228
|
+
| `-- deploy # The script to deploy to stage (run on the stage server)
|
|
229
|
+
| `-- prod # The production environment
|
|
230
|
+
| |-- config.yaml
|
|
231
|
+
| `-- deploy # The script to deploy to stage (run on the production server(s))
|
|
232
|
+
|-- private # A folder for private files (served by the file app if included)
|
|
233
|
+
`-- public # The public folder (Web server should have this as the root)
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The Waxx::Object has two purposes:
|
|
238
|
+
|
|
239
|
+
1. Specifies what fields/properties are in the table/object and what the attributes of the fields are. Like the renderer, validation, field label, etc. This is similar to a Model in MVC.
|
|
240
|
+
2. Specify the external interfaces to talk to the object's views. These are the routes and controllers combined.
|
|
241
|
+
|
|
242
|
+
If your object represents a database table, you extend with one of the following:
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
extend Waxx::Pg
|
|
246
|
+
extend Waxx::Mysql2
|
|
247
|
+
extend Waxx::Sqlite3
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Other database connectors will be added. You are welcome to make a pull request ;-)
|
|
251
|
+
|
|
252
|
+
For example:
|
|
253
|
+
|
|
254
|
+
*app/person/person.rb:*
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
module App::Person
|
|
258
|
+
extend Waxx::Pg
|
|
259
|
+
extend self
|
|
260
|
+
|
|
261
|
+
# Specify the fields/attributes
|
|
262
|
+
has(
|
|
263
|
+
id: {pkey: true, renderer: "id"},
|
|
264
|
+
first_name: {renderer: "text"},
|
|
265
|
+
last_name: {renderer: "text"},
|
|
266
|
+
email: {renderer: "email", validate: "email", required: true},
|
|
267
|
+
bio: {renderer: "html"}
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Specify what interfaces are exposed (routes) and the access control (ACL)
|
|
271
|
+
runs(
|
|
272
|
+
# Handles /person by calling list defined below
|
|
273
|
+
default: "list",
|
|
274
|
+
|
|
275
|
+
# Handles /person/list or /person because "list" is the default runner
|
|
276
|
+
list: {
|
|
277
|
+
desc: "Show a list of people",
|
|
278
|
+
acl: %w(admin), # User must be in the "admin" group to run this action
|
|
279
|
+
get: lambda{|x| List.run(x)} # How to respond to a GET request
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
# handles a request to /person/record/1
|
|
283
|
+
record: {
|
|
284
|
+
desc: "Edit a person record",
|
|
285
|
+
acl: %w(admin), # User must be in the "admin" group to run this action
|
|
286
|
+
# Each HTTP Request Type calls a different proc
|
|
287
|
+
get: ->(x, id){ Record.run(x, id) }, # SELECT
|
|
288
|
+
post: ->(x){ Record.run(x, x.req.post) }, # INSERT
|
|
289
|
+
put: ->(x, id){ Record.run(x, id, x.req.post) }, # UPDATE
|
|
290
|
+
delete: ->(x, id){ Record.run(x, id) }, # DELETE
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Require the views
|
|
296
|
+
require_relative 'list' # The List View is defined here
|
|
297
|
+
require_relative 'record' # The Record View is defined here
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
A view is like a database view (not like a Rails view). The view specifies what tables/objects and fields/properties are going to be displayed and potentially edited. The Html layout module is like a Rails view. Other layouts include: Json, Csv, Pdf, Xlsx.
|
|
301
|
+
|
|
302
|
+
**app/person/list.rb** *(This is the view that lists the users)*
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
module App::Person::List
|
|
306
|
+
extend Waxx::View
|
|
307
|
+
extend self
|
|
308
|
+
|
|
309
|
+
has(
|
|
310
|
+
:id,
|
|
311
|
+
:first_name,
|
|
312
|
+
:last_name,
|
|
313
|
+
:email
|
|
314
|
+
# This view does not include the bio field
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
module Html
|
|
318
|
+
extend Waxx::Html
|
|
319
|
+
extend self
|
|
320
|
+
|
|
321
|
+
def get(x, data, message={})
|
|
322
|
+
# This method appends to x and includes your site layout and nav.
|
|
323
|
+
# The content attribute is what goes in the content area of the page.
|
|
324
|
+
App::Html.page(x,
|
|
325
|
+
title: "People",
|
|
326
|
+
content: content(x, data)
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def content(x, data)
|
|
331
|
+
# You put your HTML output here using:
|
|
332
|
+
%(<p>HTHL or a template engine</p>)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**app/person/record.rb** *(This is the view to view, edit, update, and delete a record)*
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
module App::Person::Record
|
|
343
|
+
extend Waxx::View
|
|
344
|
+
extend self
|
|
345
|
+
|
|
346
|
+
has(
|
|
347
|
+
:id,
|
|
348
|
+
:first_name,
|
|
349
|
+
:last_name,
|
|
350
|
+
:email,
|
|
351
|
+
:bio
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
module Html
|
|
355
|
+
extend Waxx::Html
|
|
356
|
+
extend self
|
|
357
|
+
|
|
358
|
+
def get(x, data, message={})
|
|
359
|
+
App::Html.page(
|
|
360
|
+
title: "#{data['first_name']} #{data['last_name']}",
|
|
361
|
+
content: content(x, data)
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def content(x, data)
|
|
366
|
+
# You put your HTML output here using:
|
|
367
|
+
%(<p>HTHL or a template engine</p>)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def post(x)
|
|
371
|
+
# Following a post, redirect to the list view
|
|
372
|
+
x.res.redirect "/person/list"
|
|
373
|
+
end
|
|
374
|
+
alias delete post
|
|
375
|
+
alias put post
|
|
376
|
+
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
When you create a view you get four data access methods automatically. This includes:
|
|
382
|
+
|
|
383
|
+
* get
|
|
384
|
+
* get_by_id (or by_id)
|
|
385
|
+
* post
|
|
386
|
+
* put
|
|
387
|
+
* delete
|
|
388
|
+
|
|
389
|
+
Only the feilds on the view can be gotten and manipulated. For example, we can call these methods from the console:
|
|
390
|
+
|
|
391
|
+
```ruby
|
|
392
|
+
waxx console
|
|
393
|
+
person = App::Person::Record.by_id(x, 16)
|
|
394
|
+
# => A hash of the record in the table with the ID of 16
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
## Relationships
|
|
399
|
+
Relationships in Waxx are defined in the field attributes. There are INNER JOINs, LEFT JOINs, and JOINs using a Join Table (many-to-many):
|
|
400
|
+
|
|
401
|
+
### INNER JOIN (is: name:table.field)
|
|
402
|
+
|
|
403
|
+
We will add a relationship between the Person and the Company:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
module App::Person
|
|
407
|
+
extend Waxx::Pg
|
|
408
|
+
extend self
|
|
409
|
+
|
|
410
|
+
# Specify the fields/attributes
|
|
411
|
+
has(
|
|
412
|
+
id: {pkey: true, renderer: "id"},
|
|
413
|
+
company_id: {is:"company:company.id"}, # "is:" defines a relationship
|
|
414
|
+
first_name: {renderer: "text"},
|
|
415
|
+
last_name: {renderer: "text"},
|
|
416
|
+
email: {renderer: "email", validate: "email", required: true},
|
|
417
|
+
bio: {renderer: "html"}
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Then in the list view, we can add the company that the person is associated with
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
module App::Person::List
|
|
426
|
+
extend Waxx::View
|
|
427
|
+
extend self
|
|
428
|
+
|
|
429
|
+
has(
|
|
430
|
+
:id,
|
|
431
|
+
:first_name,
|
|
432
|
+
:last_name,
|
|
433
|
+
"company_name: company.name",
|
|
434
|
+
:email
|
|
435
|
+
)
|
|
436
|
+
end
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
In this case the attribute "company_name" will be added to the view and is the value of the "name" field in the company table. The syntax for this is `<name>: <relationship_name (as defined in the object)>.<field>`.
|
|
440
|
+
|
|
441
|
+
### LEFT JOIN (is: name:table.field+)
|
|
442
|
+
|
|
443
|
+
We will add an invoice and invoice_item table.
|
|
444
|
+
|
|
445
|
+
**Invoice Object**
|
|
446
|
+
|
|
447
|
+
```
|
|
448
|
+
module App::Invoice
|
|
449
|
+
extend Waxx::Obj
|
|
450
|
+
extend self
|
|
451
|
+
|
|
452
|
+
# Specify the fields/attributes
|
|
453
|
+
has(
|
|
454
|
+
id: {pkey: true, is:"items:invoice_item.invoice_id+"},
|
|
455
|
+
customer_id: {is:"company:company.id", required: true},
|
|
456
|
+
invoice_date: {renderer: "date", required: true},
|
|
457
|
+
terms: {renderer: "text", required: true},
|
|
458
|
+
status: {renderer: "select", default: "Draft"}
|
|
459
|
+
)
|
|
460
|
+
end
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
*Note: The + sign after the related attribute make this join a left join (Oracle style)*
|
|
464
|
+
|
|
465
|
+
INNER JOIN (If you don't want to show invoices with no items):
|
|
466
|
+
|
|
467
|
+
`id: {pkey: true, is:"items: invoice_item.invoice_id"}`
|
|
468
|
+
|
|
469
|
+
LEFT JOIN (If you want to show invoices with no items):
|
|
470
|
+
|
|
471
|
+
`id: {pkey: true, is:"items: invoice_item.invoice_id+"}`
|
|
472
|
+
|
|
473
|
+
**InvoiceItem Object**
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
module App::InvoiceItem
|
|
477
|
+
extend Waxx::Pg
|
|
478
|
+
extend self
|
|
479
|
+
|
|
480
|
+
# Specify the fields/attributes
|
|
481
|
+
has(
|
|
482
|
+
id: {pkey: true, renderer: "id"},
|
|
483
|
+
invoice_id: {is: "invoice:invoice.id", required: true},
|
|
484
|
+
product_id: {is: "product:product.id", required: true},
|
|
485
|
+
description: {renderer: "text"},
|
|
486
|
+
quantity: {renderer: "number"},
|
|
487
|
+
unit_price: {renderer: "money"}
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Invoice::Items View**
|
|
493
|
+
This will show a list of all invoices and the items on the invoices:
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
module App::Invoice::Items
|
|
497
|
+
extend Waxx::View
|
|
498
|
+
extend self
|
|
499
|
+
|
|
500
|
+
has(
|
|
501
|
+
:id,
|
|
502
|
+
:invoice_date,
|
|
503
|
+
"company: company.name",
|
|
504
|
+
"product: product.name",
|
|
505
|
+
"desc: items.description",
|
|
506
|
+
"qty: items.quantity",
|
|
507
|
+
"price: items.unit_price",
|
|
508
|
+
{name: "total", sql_select: "items.quantity * items.unit_price"}
|
|
509
|
+
)
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
This will generate the following SQL:
|
|
514
|
+
|
|
515
|
+
```sql
|
|
516
|
+
SELECT invoice.id, invoice.invoice_date, company.name as company, product.name as product,
|
|
517
|
+
items.description as desc, items.quantity as qty, items.unit_price as price,
|
|
518
|
+
(items.quantity * items.unit_price) as total
|
|
519
|
+
FROM invoice
|
|
520
|
+
LEFT JOIN invoice_item AS items ON invoice.id = invoice_item.invoice_id
|
|
521
|
+
INNER JOIN company ON invoice.customer_id = company.id
|
|
522
|
+
INNER JOIN product ON items.product_id = product.id
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
The following attributes can be used in your layout (output)
|
|
526
|
+
|
|
527
|
+
`id, invoice_date, company, product, desc, qty, price, total`
|
|
528
|
+
|
|
529
|
+
### Many-to-Many Relationships
|
|
530
|
+
|
|
531
|
+
The join table is just another object in Waxx
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
# The Usr Object
|
|
535
|
+
module App::Usr
|
|
536
|
+
extend Waxx::Pg
|
|
537
|
+
extend self
|
|
538
|
+
|
|
539
|
+
has({
|
|
540
|
+
id: {pkey: true, is:"group_member: usr_grp.usr_id+"},
|
|
541
|
+
email: {validate: "email"},
|
|
542
|
+
password_sha256 {renderer: "password", encrypt: "sha256", salt: true}
|
|
543
|
+
})
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# The Grp Object
|
|
547
|
+
module App::Grp
|
|
548
|
+
extend Waxx::Pg
|
|
549
|
+
extend self
|
|
550
|
+
|
|
551
|
+
has({
|
|
552
|
+
id: {pkey: true, is:"group_members: usr_grp.grp_id+"},
|
|
553
|
+
name: {required: true}
|
|
554
|
+
})
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# The Usr->Grp Join Table
|
|
558
|
+
module App::UsrGrp
|
|
559
|
+
extend Waxx::Pg
|
|
560
|
+
extend self
|
|
561
|
+
|
|
562
|
+
has({
|
|
563
|
+
id: {pkey: true},
|
|
564
|
+
usr_id: {required: true, is:"usr:usr.id"},
|
|
565
|
+
grp_id: {required: true, is:"grp:grp.id"}
|
|
566
|
+
})
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# View that joins all three tables (show all users and groups they are in)
|
|
570
|
+
module App::Usr::Groups
|
|
571
|
+
extend Waxx::View
|
|
572
|
+
extend self
|
|
573
|
+
|
|
574
|
+
has(
|
|
575
|
+
:id,
|
|
576
|
+
:email,
|
|
577
|
+
"group_id: group_member.grp_id",
|
|
578
|
+
"group: grp.name"
|
|
579
|
+
)
|
|
580
|
+
end
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
Some explanation of the View:
|
|
584
|
+
|
|
585
|
+
* **group_id** is the name of the field on the view (you choose the name).
|
|
586
|
+
* **group_member.grp_id** causes the join table **usr_grp** to be LEFT JOINed in because the relationship "**group_member**" is defined in the attributes of **App::Usr.id**.
|
|
587
|
+
* **group** is the name of the group. (You define this as you please. Could be group_name just as well.)
|
|
588
|
+
* **grp** matched the **grp** relationship defined in the **App::UsrGrp.grp_id** field and causes and INNER JOIN on the **grp** table.
|
|
589
|
+
* The **group_members** relationship in **App::Grp.id** and the **usr** relationship in **App::UsrGrp.usr_id** are not used in this case because we start with `usr` and include `usr_grp` and then `grp`. If we started with `grp` and included `usr_grp` and `usr`, then those relationships would be used. If you are going in only one direction in your app, then you only need to define the relationships in the direction you are using.
|
|
590
|
+
|
|
591
|
+
The resulting SQL:
|
|
592
|
+
|
|
593
|
+
```sql
|
|
594
|
+
SELECT usr.id, usr.email, group_member.grp_id AS group_id, grp.name AS group
|
|
595
|
+
FROM usr
|
|
596
|
+
LEFT JOIN usr_grp AS group_member ON usr.id = group_member.usr_id
|
|
597
|
+
JOIN grp ON group_member.grp_id = grp.id
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
The view will show all users and any groups they are in.
|
|
602
|
+
|
|
603
|
+
## Routing
|
|
604
|
+
|
|
605
|
+
Waxx is closer to an RPC (remote procedure call) system than a routed system.
|
|
606
|
+
|
|
607
|
+
### Arguments
|
|
608
|
+
|
|
609
|
+
`example.com/artist/list` maps to `app = "artist"` and `act = "list"` and will call the list method defined in App::Artist.runs().
|
|
610
|
+
|
|
611
|
+
Each slash-delimited argument after the first two are treated as arguments to the function:
|
|
612
|
+
|
|
613
|
+
`/artist/in/us/california/los-angeles` will feed into the following runner:
|
|
614
|
+
|
|
615
|
+
```
|
|
616
|
+
module App::Artist
|
|
617
|
+
extend Waxx::Pg
|
|
618
|
+
extend self
|
|
619
|
+
|
|
620
|
+
runs(
|
|
621
|
+
in: {
|
|
622
|
+
desc: "Show a list of artists in an area",
|
|
623
|
+
get: lambda{|x, country, state_prov, city|
|
|
624
|
+
List.run(x, args: {country: country, state_prov: state_prov, city: city})
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
)
|
|
628
|
+
end
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
In this case all three parameters are required. An error will be raised if the city is missing.
|
|
632
|
+
There are two options: Add default values or use a proc instead of a lambda:
|
|
633
|
+
|
|
634
|
+
```
|
|
635
|
+
get: proc{|x, country, state_prov, city| }
|
|
636
|
+
# If city is missing: /artist/in/us/colorado, then city will be nil
|
|
637
|
+
|
|
638
|
+
get: lambda{|x, country="us", state_prov="", city=""| }
|
|
639
|
+
# If city is missing: /artist/in/us/colorado, then city will be "" or whatever you set the default to
|
|
640
|
+
|
|
641
|
+
get: -> (x, country="us", state_prov="", city="") { }
|
|
642
|
+
# This is equivilant to the lambda example above
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
NOTE: You can use `return` in `lambda` and `->` constructs, but you need to use `break` in `proc` constructs to stop processing.
|
|
646
|
+
|
|
647
|
+
### Variable Act / not_found
|
|
648
|
+
|
|
649
|
+
What if you want the act be a variable like `/artist/david-bowie` or `/artist/motorhead`?
|
|
650
|
+
|
|
651
|
+
You define **`not_found`** in your Object runs method:
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
module App::Artist
|
|
655
|
+
extend Waxx::Pg
|
|
656
|
+
extend self
|
|
657
|
+
|
|
658
|
+
runs(
|
|
659
|
+
default: "list",
|
|
660
|
+
list: {
|
|
661
|
+
desc: "Show a list of artists: /artist or /artist/list",
|
|
662
|
+
get: lambda{|x|
|
|
663
|
+
# Sort the results based on the query string: /artist?order=name
|
|
664
|
+
List.run(x, order: x['order'])
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
profile: {
|
|
668
|
+
desc: "Show an artist profile based on their slug in the URL: /artist/profile/<slug>",
|
|
669
|
+
get: lambda{|x, artist_slug|
|
|
670
|
+
# Set the slug attribute from the passed in variable
|
|
671
|
+
Profile.run(x, args: {slug: artist_slug})
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
not_found: {
|
|
675
|
+
desc: "Show an artist profile based on their slug in the URL: /artist/<slug>",
|
|
676
|
+
get: lambda{|x|
|
|
677
|
+
# Set the slug attribute to the act
|
|
678
|
+
Profile.run(x, args: {slug: x.act})
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
)
|
|
682
|
+
end
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Note: In the above example `/artist/led-zeppelin` and `/artist/profile/led-zeppelin` will show the same result. (For SEO you should only use one of these or include a canonical meta attribute.)
|
|
686
|
+
|
|
687
|
+
There is also a `not_found` method defined at the top level as well. By default Waxx will look for a website_page where the URI matches the website_page.uri. You can change this behavior by adding a `App.not_found` method to `app/app.rb`.
|
|
688
|
+
|
|
689
|
+
## Access Control
|
|
690
|
+
|
|
691
|
+
Waxx includes a full user and session management system. The following apps are installed by default:
|
|
692
|
+
|
|
693
|
+
```
|
|
694
|
+
app/grp
|
|
695
|
+
app/usr
|
|
696
|
+
app/usr/grp
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
Using these apps allow you to add users and groups and put users in groups. You define your access control lists for each method. There are several levels of permissions. The following seven code blocks are parts of the same file:
|
|
700
|
+
|
|
701
|
+
### Example ACLs
|
|
702
|
+
ACLs are defined as a attribute (`acl: [nil|string|array|hash|lambda]`) of each method options hash.
|
|
703
|
+
|
|
704
|
+
The following code blocks are different examples of the acl attribute in practice.
|
|
705
|
+
|
|
706
|
+
**Start: app/product/product.rb**
|
|
707
|
+
|
|
708
|
+
```
|
|
709
|
+
module App::Product
|
|
710
|
+
extend Waxx::Pg
|
|
711
|
+
extend self
|
|
712
|
+
|
|
713
|
+
runs(
|
|
714
|
+
default: "list",
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
#### Public
|
|
718
|
+
|
|
719
|
+
No ACL defined:
|
|
720
|
+
|
|
721
|
+
```
|
|
722
|
+
list: {
|
|
723
|
+
desc: "Show a list of products (public)",
|
|
724
|
+
# No acl attribute so it is public
|
|
725
|
+
get: lambda{|x|
|
|
726
|
+
List.run(x, order: x['order'])
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
#### Any logged in user
|
|
732
|
+
|
|
733
|
+
```
|
|
734
|
+
exclusives: {
|
|
735
|
+
desc: "Show a list of exclusive products",
|
|
736
|
+
acl: "user", # The name of the quasi group "user" (anyone who is logged in)
|
|
737
|
+
get: lambda{|x|
|
|
738
|
+
List.run(x, order: x['order'])
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### In any group:
|
|
744
|
+
User must be in one of the groups listed
|
|
745
|
+
|
|
746
|
+
```
|
|
747
|
+
private: {
|
|
748
|
+
desc: "Show a list of private products",
|
|
749
|
+
acl: %w(big_spender deal_seaker admin product_manager),
|
|
750
|
+
get: lambda{|x|
|
|
751
|
+
List.run(x, order: x['order'])
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
#### In a group depending on request method:
|
|
757
|
+
User must be in one of the groups listed to run a specific request method
|
|
758
|
+
|
|
759
|
+
```
|
|
760
|
+
record: {
|
|
761
|
+
desc: "Show a list of products (public)",
|
|
762
|
+
acl: {
|
|
763
|
+
get: %w(user), # Any logged in user can GET
|
|
764
|
+
post: %w(admin product_manager) # Only admin and product_manager can POST
|
|
765
|
+
},
|
|
766
|
+
get: lambda{|x, id|
|
|
767
|
+
Record.run(x, id: id)
|
|
768
|
+
},
|
|
769
|
+
post: lambda{|x, id|
|
|
770
|
+
Record.run(x, id: id, data: x.req.post)
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
#### Lambda/Proc (total control ACL):
|
|
776
|
+
If the proc or lambda returns true, then the user is allowed to proceed, otherwise an error is returned. The proc is passed `x`
|
|
777
|
+
|
|
778
|
+
```ruby
|
|
779
|
+
special: {
|
|
780
|
+
desc: "View and edit a product from a specific IP
|
|
781
|
+
or if the user has a secret key in their session",
|
|
782
|
+
acl: -> (x) {
|
|
783
|
+
x.req.env['X-REAL-IP'] == "10.10.10.10" or x.usr['secret'] == "let-me-in"
|
|
784
|
+
},
|
|
785
|
+
get: -> (x, id) { Record.run(x, id: id) },
|
|
786
|
+
post: -> (x, id) { Record.run(x, id: id, data: x.req.post) }
|
|
787
|
+
},
|
|
788
|
+
|
|
789
|
+
mine: {
|
|
790
|
+
desc: "View and edit a product owned by the user",
|
|
791
|
+
acl: -> (x) {
|
|
792
|
+
# Get the product.owner_id from the database
|
|
793
|
+
product = by_id(x, x.oid, "owner_id")
|
|
794
|
+
# Return true if the logged in user is the owner
|
|
795
|
+
product['owner_id'] == x.usr['id']
|
|
796
|
+
},
|
|
797
|
+
get: -> (x, id) { Record.run(x, id: id) },
|
|
798
|
+
post: -> (x, id) { Record.run(x, id: id, data: x.req.post) }
|
|
799
|
+
},
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
End the object file
|
|
803
|
+
|
|
804
|
+
```
|
|
805
|
+
)
|
|
806
|
+
end
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**End: app/product/product.rb**
|
|
810
|
+
|
|
811
|
+
## Quick Examples
|
|
812
|
+
|
|
813
|
+
A fast JSON response for an autocomplete form field
|
|
814
|
+
|
|
815
|
+
If you want to have a quick JSON response for an autocomplete -- Just use a Waxx::Object and bypass the Waxx::View and layout (Json, HTML, etc.).
|
|
816
|
+
Direct access is available to the database driver with `x.db.app` where `app` is the name of the database connection defined in your config.yaml file.
|
|
817
|
+
In this case, as a user types in an autocomplete input box, the browser sends a request to: `/artist/autocomplete.json?q=da`
|
|
818
|
+
When the .json extension is used, the response content type will be application/json.
|
|
819
|
+
What the user types would be in the `q` attribute.
|
|
820
|
+
|
|
821
|
+
PostgreSQL DB:
|
|
822
|
+
|
|
823
|
+
```ruby
|
|
824
|
+
module App::Artist
|
|
825
|
+
extend Waxx::Object
|
|
826
|
+
extend self
|
|
827
|
+
|
|
828
|
+
# Notice that "has" is not specified so you can't use waxx get and post methods.
|
|
829
|
+
# You are just talking straight to the database and formatting the output as json
|
|
830
|
+
# and sending that straight to x. The `x['q']` is the value of the q query parameter.
|
|
831
|
+
|
|
832
|
+
runs(
|
|
833
|
+
autocomplete: {
|
|
834
|
+
desc: "Show a list of artists that match the 'q' param",
|
|
835
|
+
get: -> (x) {
|
|
836
|
+
x << x.db.app.exec("
|
|
837
|
+
SELECT id, name
|
|
838
|
+
FROM artist
|
|
839
|
+
WHERE name ILIKE $1
|
|
840
|
+
ORDER BY name
|
|
841
|
+
LIMIT 20",
|
|
842
|
+
["#{x['q']}%"]
|
|
843
|
+
).map{|rec| rec }.to_json
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
)
|
|
847
|
+
end
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
If you are using Mongo, you can do it like this:
|
|
851
|
+
|
|
852
|
+
```ruby
|
|
853
|
+
module App::Artist
|
|
854
|
+
extend Waxx::Object
|
|
855
|
+
extend self
|
|
856
|
+
|
|
857
|
+
runs(
|
|
858
|
+
autocomplete: {
|
|
859
|
+
desc: "Show a list of artists that match the 'q' param",
|
|
860
|
+
get: -> (x) {
|
|
861
|
+
x << x.db.app['artist']
|
|
862
|
+
.find({name: /^#{x['q']}/})
|
|
863
|
+
.projection({name:1}) # you get _id automatically
|
|
864
|
+
.sort({name:1})
|
|
865
|
+
.limit(20)
|
|
866
|
+
.map{|rec| rec }.to_json
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
)
|
|
870
|
+
end
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
Both of these should return a response in less than one millisecond (assuming your data is indexed and running on descent hardware).
|
|
874
|
+
|
|
875
|
+
That is the intro. Give it a whirl.
|
|
876
|
+
|
|
877
|
+
Please send any feedback to dan@waxx.io
|
|
878
|
+
|
|
879
|
+
|