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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE +201 -0
  4. data/README.md +879 -0
  5. data/bin/waxx +120 -0
  6. data/lib/waxx/app.rb +173 -0
  7. data/lib/waxx/conf.rb +54 -0
  8. data/lib/waxx/console.rb +204 -0
  9. data/lib/waxx/csrf.rb +14 -0
  10. data/lib/waxx/database.rb +80 -0
  11. data/lib/waxx/encrypt.rb +38 -0
  12. data/lib/waxx/error.rb +60 -0
  13. data/lib/waxx/html.rb +33 -0
  14. data/lib/waxx/http.rb +268 -0
  15. data/lib/waxx/init.rb +273 -0
  16. data/lib/waxx/irb.rb +44 -0
  17. data/lib/waxx/irb_env.rb +18 -0
  18. data/lib/waxx/json.rb +23 -0
  19. data/lib/waxx/mongodb.rb +221 -0
  20. data/lib/waxx/mysql2.rb +234 -0
  21. data/lib/waxx/object.rb +115 -0
  22. data/lib/waxx/patch.rb +138 -0
  23. data/lib/waxx/pdf.rb +69 -0
  24. data/lib/waxx/pg.rb +246 -0
  25. data/lib/waxx/process.rb +270 -0
  26. data/lib/waxx/req.rb +116 -0
  27. data/lib/waxx/res.rb +98 -0
  28. data/lib/waxx/server.rb +304 -0
  29. data/lib/waxx/sqlite3.rb +237 -0
  30. data/lib/waxx/supervisor.rb +47 -0
  31. data/lib/waxx/test.rb +162 -0
  32. data/lib/waxx/util.rb +57 -0
  33. data/lib/waxx/version.rb +3 -0
  34. data/lib/waxx/view.rb +389 -0
  35. data/lib/waxx/waxx.rb +73 -0
  36. data/lib/waxx/x.rb +103 -0
  37. data/lib/waxx.rb +50 -0
  38. data/skel/README.md +11 -0
  39. data/skel/app/app/app.rb +39 -0
  40. data/skel/app/app/error/app_error.rb +16 -0
  41. data/skel/app/app/error/dhtml.rb +9 -0
  42. data/skel/app/app/error/html.rb +8 -0
  43. data/skel/app/app/error/json.rb +8 -0
  44. data/skel/app/app/error/pdf.rb +13 -0
  45. data/skel/app/app/log/app_log.rb +13 -0
  46. data/skel/app/app.rb +20 -0
  47. data/skel/app/home/home.rb +16 -0
  48. data/skel/app/home/html.rb +145 -0
  49. data/skel/app/html.rb +192 -0
  50. data/skel/app/usr/email.rb +66 -0
  51. data/skel/app/usr/html.rb +115 -0
  52. data/skel/app/usr/list.rb +51 -0
  53. data/skel/app/usr/password.rb +54 -0
  54. data/skel/app/usr/record.rb +98 -0
  55. data/skel/app/usr/usr.js +67 -0
  56. data/skel/app/usr/usr.rb +277 -0
  57. data/skel/app/waxx/waxx.rb +109 -0
  58. data/skel/bin/README.md +1 -0
  59. data/skel/db/README.md +11 -0
  60. data/skel/db/app/0-init.sql +88 -0
  61. data/skel/lib/README.md +1 -0
  62. data/skel/log/README.md +1 -0
  63. data/skel/opt/dev/config.yaml +1 -0
  64. data/skel/opt/prod/config.yaml +1 -0
  65. data/skel/opt/stage/config.yaml +1 -0
  66. data/skel/opt/test/config.yaml +1 -0
  67. data/skel/private/README.md +1 -0
  68. data/skel/public/lib/site.css +202 -0
  69. data/skel/public/lib/waxx/w.ico +0 -0
  70. data/skel/public/lib/waxx/w.png +0 -0
  71. data/skel/public/lib/waxx/waxx.js +111 -0
  72. data/skel/tmp/pids/README.md +1 -0
  73. data.tar.gz.sig +0 -0
  74. metadata +140 -0
  75. 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
+