UNPKG

chocolate

Version:

A full stack Node.js web framework built using Coffeescript

1,538 lines (1,028 loc) 71.3 kB
()) (( ) ) _____ _ ( )( ) ) / ____| | _ (.( ) .)(.( )) _ | | | |__ ___ ___ ___ ( ( ).( ) (.)( ).) ) | | | '_ \ / _ \ / __/ _ \ |`-..___________ ..-'| | |____| | | | (_) | (_| (_) | \ / \_____|_| |_|\___/ \___\___/ | ;---. | (__ \ | | | | (_) | | ) ) | | __ _| |_ ___ _ ___ | | / / | | / _` | __/ _ \ | / __| | ( / | |___| (_| | || __/_| \__ \ / \ _/ |______\__,_|\__\___(_) |___/ | | _/ | `-..____________..-' |__/ ## Chocolate - Full stack Node.js framework Chocolate is an experimental and isomorphic Node.js webapp framework built using Coffeescript. It includes : - **Chocolate Studio** -- an **online IDE** (with Coffeescript, Javascript, Css, Json, and Markdown support) - **Locco** -- the Chocolate protocol : so, what, where, how... - **LateDB** -- a kind of database running in-memory and logged to disk - **Chocokup** -- a 100% pure CoffeeScript templating language that helps build web user interfaces (based on Coffeekup) - **Chocodown** -- Chocokup-aware port of Markdown (based on Showdown) - **Chocolate Lab** -- an online and immediate **Lab** playground where you write, transpile and/or test code between Javascript and Coffeescript and also between Html and Chocokup... - **Specolate** -- a behavior/test driven development tool (based on Jasmine) that works client and server side - **Doccolate** -- an online documentation editing tool (based on Docco) - **Chocodash** -- toolbox with javascript object identity, types, serialization and asynchronous calls and signals management - **liteJq** -- a lite jQuery-compatible library - **liteLorem** -- a basic lorem (fake words, sentences and images) library - an automatic **free SSL certificate** service with Let's Encrypt - a simple **reverse proxy** service - a basic **source control** with Git - **Chocoss** -- a Css framework - **NewNotes** -- a promising note taking tool Chocolate integrates: > [Node.js](http://nodejs.org) - [Coffeescript](http://coffeescript.org) - [Ace](http://ace.ajax.org) - [Letsencrypt](https://github.com/Daplie/node-letsencrypt) - [Http-proxy](https://github.com/nodejitsu/node-http-proxy) - [Jasmine](http://pivotal.github.com/jasmine) - [Reactor](https://github.com/fynyky/reactor.js) > > [Coffeekup](http://coffeekup.org) - [Showdown](https://github.com/coreyti/showdown) - [Highlight.js](http://softwaremaniacs.org/soft/highlight/en) - [Docco](http://jashkenas.github.com/docco/) - [Ccss](https://github.com/aeosynth/ccss) - [Git](http://git-scm.com) - [Impress](http://bartaz.github.com/impress.js/#/bored) &nbsp; --- ## Version **Chocolate v0.0.33 - (2020-12-18)** --- UPDATES - in `server/workflow`: - we now are compatbile with Letsencrypt v2 through the use of ACME.js, a low-level client for Let's Encrypt built by Root (https://therootcompany.com/) - in `server/monitor`: - We do not use `node-inspector` anymore but we now uses V8's integrated debugging service - So to start a debugging session you will now need to: - open a ssh tunnel to your remote server (ssh -L 9229:localhost:9229 user@remote.example.com) - open chrome tab with `chrome://inspect#devices` - in `general/latedb`: - You can now directly access the raw data stored in a lateDB table db.tables.get(table_name) - You can also directly query a field's index in a LatDB table: db.tables.get(table_name, id, index) Be carefull: `db.tables.get` returns the raw data stored in LateDB, you'd rather use `db.tables.query` that returns a filtered and cloned version of the data. - Tables' fields are now collected automatically in LateDB and can also be added, removed and listed manually To get a table's fields list: db.tables.list(table_name) To add a field in a table's fields list: db.tables.alter(table_name, {add:'field'}) To remove a field from a table's fields list: db.tables.alter(table_name, {drop:'field'}) FIXED BUGS - in `server/monitor`: - filtered dot files out of monitoring - filtered files inside `node_modules` directory out of monitoring - error messages received were wrongly and badly rerouted to stdout - in `general/latedb`: - was not able to compact client-side database when no timestamp was given - was not able to clone null values (introduced in 0.0.31) - in `server/studio`: - `diff` tab was not displaying diff for source files with an associated spec file - in bin/chocomake, ssl key length was increased to 2048 UPDATES - updated Chokidar to v 3.4.0 - updated Chocolate's logo in README.md file See history in **CHANGELOG.md** file &nbsp; --- ## <a name="Choco-Summary"></a> Summary - [Demo](#Choco-Demo) - [Installation](#Choco-Installation) - [Use it](#Choco-UseIt) - [Log on](#Choco-UseIt-LogOn) - [Log off](#Choco-UseIt-LogOff) - [Enter Chocolate Studio](#Choco-UseIt-Enter) - [Web access to source files and functions](#Choco-UseIt-Source) - [Locco main operations](#Choco-UseIt-Locco) - [Chocolate Studio](#Choco-Studio) - [Autocomplete and snippets](#Choco-Automplete) - [Spec, Doc, Lab, Help and Notes panels](#Choco-Studio-panels) - [The Lab](#Choco-Lab) - [How to write Modules](#Choco-WriteModules) - [LateDB](#Late-DB) - [Chocodash](#Choco-Dash) - [Javascript type management](#Choco-Dash-Type) - [Better than Class with Prototypes](#Choco-Dash-Prototype) - [Defaulting properties](#Choco-Dash-Defaults) - [Reactive services with Signals](#Choco-Dash-Reactive) - [Asynchronous calls made easy](#Choco-Dash-Async) - [Javascript object serialization](#Choco-Dash-Stringify) - [Uuid](#Choco-Dash-Uuid) - [Debugate](#Choco-Debugate) - [Chocokup](#Choco-Chocokup) - [Usage](#Choco-Chocokup-Usage) - [Chocokup.Document](#Choco-Chocokup-Document) - [Chocokup.Panel](#Choco-Chocokup-Panel) - [Reference](#Choco-Chocokup-Reference) - [Chocoss](#Choco-Chocoss) - [Locco](#Choco-Locco) - [Protocol operations](#Choco-Locco-operations) - [Interface](#Choco-Locco-Interface) - [Interface.Web](#Choco-Locco-Interface-Web) - [Specolate](#Choco-Specolate) - [Usage](#Choco-Specolate-Usage) - [Doccolate](#Choco-Doccolate) - [litejQ](#Choco-litejQ) - [liteLorem](#Choco-liteLorem) - [Newnotes](#Choco-Newnotes) - [Newnotes-Usage](#Choco-Newnotes-Usage) - [Impress.js with Newnotes !](#Choco-Newnotes-Impress) - [Reference](#Choco-Newnotes-Reference) - [Road Map](#Choco-RoadMap) - [License](#Choco-License) &nbsp; --- ## <a name="Choco-Demo"></a> Demo [⌂](#Choco-Summary) There is a non-writable demo at : <https://demo.chocolatejs.org/> --- ## <a name="Choco-Installation"></a> Installation [⌂](#Choco-Summary) This procedure was tested as **root** on Debian 8.0 ### Prerequisites Chocolate needs Node.js (from v0.10.22 to latest). **Install Node.js (v6.x)** apt-get update apt-get upgrade apt-get install curl curl -sL https://deb.nodesource.com/setup_6.x | bash - apt-get install -y nodejs **Make node modules accessible everywhere** You can use start, stop and monitor Chocolate's app with PM2 service: **Install PM2** npm install -g pm2 &nbsp; Chocolate also needs: **Other prerequisites** apt-get install g++ apt-get install git npm install -g coffee-script &nbsp; ### Install Chocolate: npm install -g --unsafe-perm chocolate &nbsp; **Run chocomake to create myapp** cd /home chocomake myapp Answer asked questions to create a self-signed SSL certificate. &nbsp; **Install Chocolate in PM2** su - myapp **Start 'myapp'** pm2 start coffee --name="myapp" -- /usr/lib/node_modules/chocolate/server/monitor.coffee /home/myapp pm2 save pm2 startup ctrl+d -- then execute the command that was displayed **To stop, start or restart 'myapp'** pm2 stop myapp pm2 start myapp pm2 restart myapp &nbsp; --- ## <a name="Choco-UseIt"></a> Use it [⌂](#Choco-Summary) Chocolate runs on your server and responds to https requests on port 8026 You can change port number in the `pm2 start` command where you append the **port** parameter: pm2 start coffee --name="myapp" -- /usr/lib/node_modules/chocolate/server/monitor.coffee /home/myapp 8081 You can also use a simple Http server by specifying options in the `/home/myapp/data/app.config.json` file: http_only: true port: 80 ### <a name="Choco-UseIt-LogOn"></a> Log on [⌂](#Choco-Summary) You defined a master key when using chocomake to create myapp. You enter that key at: https://myserver:8026/-/server/interface?register_key ### <a name="Choco-UseIt-LogOff"></a> Log off [⌂](#Choco-Summary) To logoff go to : https://myserver:8026/-/server/interface?forget_keys ### <a name="Choco-UseIt-Enter"></a> Enter Chocolate Studio [⌂](#Choco-Summary) To enter Chocolate Studio, go to: https://myserver:8026/-/server/studio There you can create, modify, move and commit source files ### <a name="Choco-UseIt-Source"></a> Web access to source files and functions [⌂](#Choco-Summary) You access a file directly in the browser: To display `default.coffee` as raw text https://myserver:8026/default?how=raw To display `default.coffee` as documentation text (docco) https://myserver:8026/default?how=help To edit `default.coffee` https://myserver:8026/default?how=edit To run `default.spec.coffee` specs (if you create it) https://myserver:8026/default?so=eval ### <a name="Choco-UseIt-Locco"></a> Locco main operations [⌂](#Choco-Summary) Requests to Chocolate server follow theses rules (the [Locco](#Choco-Locco) protocol main operations): **https** By default, Chocolate uses Https: Http requests are redirected to https Https server is located (by default) at port 8026 Http server is located at port Https+1 (8027) You can specify options in `data/app.config.json` file: http_only: true or false port: <main port number> key: <key filename> cert: <cert filename> **Let's Encrypt SSL certificate** You can use [Let's Encrypt](https://letsencrypt.org/) free SSL certificate service directly in your Chocolate app: First configure Let's Encrypt's service in `data/app.config.json`: "letsencrypt": { "domains": [ "yourdomain.com" ], "email": "you@yourdomain.com", "agreeTos": true, "production": true, } You can put `false` in `production` parameter to test certificate generation. The generated certificates should appear in `data/letsencrypt/live/yourdomain` folder Your certificate will then be renewed and the app restarted, automatically after approximately 90 days But there is more: you can put many domains in the same certificate "domains": [ "yourdomain.com", "theirdomain.com", "ourdomain.com" ] Finally, you **have** to explicitly add an entry with `yourdomain` prefixed with `www` if you want to support it: "domains": [ "yourdomain.com", "www.yourdomain.com" ] **Reverse Proxy service** There is a simple Reverse Proxy service that you can configure in `data/app.config.json`: "proxy": ['yourdomain.com', 'theirdomain.com', 'ourdomain.com'] Then `Chocolate` will forward request for those domains to local processes/apps awaiting requests on your proxy app port + 10 So if your proxy app is on `8026` port then `yourdomain.com` will be on `8036`, `theirdomain.com` will be on `8046`... And if you also use `Chocolate`'s `letsencrypt` feature, you'll only have to set: "proxy": true and `Chocolate` will use the domains defined in "letsencrypt": { "domains": [ "yourdomain.com" ], ... **Javascript Bundle service** You can define Javascript bundles to be built when source files are saved. If you have some client side Coffeescript/Javascript files (in Client or General folders) with the same prefix (or in the same subfolder), they can be bundled in the same file. In the `app.config.json` file, add a `build:{bundles:[]}` section, with the following parameters: filename: the name for the output bundle file prefix: the prefix used in (or the path to) every file to put in the bundle known_files: an array of files' path, that have to be put in that precise order in the bundle with_modules: true or false, to put in the bundle the necessary code to make those files required by the Chocolate's require service "build": { "bundles": [ { "filename": "locco.js", "prefix": "locco", "known_files": { "locco/intention.js": true, "locco/data.js": true, "locco/action.js": true, "locco/document.js": true, "locco/workflow.js": true, "locco/interface.js": true, "locco/actor.js": true, "locco/reserve.js": true, "locco/prototype.js": true }, "with_modules": true } ] } **Chocolate system services and files** They are accessible (if you registered the master key) at: https://myserver:8026/-/server... https://myserver:8026/-/general... **Your app services and files** They are at: https://myserver:8026/myservice... https://myserver:8026/mydir/myservice... **Default service in source file** If your source file exports an `interface` function (ie. in `default.coffee`): exports.interface = () -> 'Hello world!' Then it is called when you request that file with no parameter: https://myserver:8026/default returns a web page with Hello world! You can use the [Interface.Web](#Choco-Interface-Web) service with [Chocokup](#Choco-Chocokup) to produce your Html page : Interface = require 'chocolate/general/locco/interface' exports.interface = new Interface.Web -> div "Hello world #{world}!" for world in [1..5] &nbsp; --- ## <a name="Choco-Studio"></a> Chocolate Studio [⌂](#Choco-Summary) A sweet web app development environment. It displays your source files and browse through directories, has a search in files service. It has a panel that displays log messages. It can also list and open source file commited versions. You can create, move, rename and delete files. The central panel has the code editor. It has syntax highlighting for Coffeescript, Javascript, CSS and Markdown. <a name="Choco-Automplete"></a> ### <a name="Choco-Studio"></a> Autocomplete and Snippets [⌂](#Choco-Summary) It has a basic automplete feature that, by pressing CTRL+SPACE keys, proposes you a list of words collected from your file. It also has snippets that will expand code from a shortcut: in an HTML file, html5+CTRL+SPACE will become: <!DOCTYPE html> <html> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <title>`substitute(Filename('', 'Page Title'), '^.', '\u&', '')`</title> meta </head> <body> body </body> </html> Then you can move to *meta* and *body* section by pressing the TAB key. Currently there are Coffeescript, Javascript, CSS and HTML snippets in the editor. <a name="Choco-Studio-panels"></a> ### Spec, Doc, Lab, Help and Notes panels [⌂](#Choco-Summary) The central panel can also split to display the associated spec file (see [Specolate](#Choco-Specolate)) or the source file in help mode (see [Doccolate](#Choco-Doccolate)) or the [Lab](#Choco-Lab) that can be used to test Coffeescript or Chocodown code. The help panel lists some usefull resources. Links will be opened in the central panel. The [Notes](#Choco-Newnotes) panel allows you to write and save some notes. **Usage** https://myserver:8026/-/server/studio **Source** https://myserver:8026/-/server/studio?how=raw **Editor shrotcuts** https://github.com/ajaxorg/ace/wiki/Default-Keyboard-Shortcuts &nbsp; --- ## <a name="Choco-Lab"></a> The Lab [⌂](#Choco-Summary) The Lab helps you write your code and test cases, syntax and also translate between Javascript and Coffeescript and also between Html and Chocokup... **Coffeescript Lab** When you type Coffescript code in the Lab editor, it is immediately translated in Javascript. You can use this service to learn Coffeescript but also to verify that your code will do what you expect it should. Beside beeing translated in Javascript it is also immediately executed. And you can see the result in the terminal panel. But more... when you display the **Debug** panel you can see your variables values through code execution! This service is inspired by Bret Victor's [lecture](http://vimeo.com/36579366) (Inventing on priciples). Copy the following code in the Coffeescript Lab with the Debug panel: binarySearch = (key, array) -> low = 0 high = array.length-1 while (low <= high) mid = Math.floor((low + high) / 2) value = array[mid] if value > key high = mid - 1 else if value < key low = mid + 1 else return mid return -1 result = binarySearch 'f', ['a', 'b', 'c', 'd', 'e','f'] Then change the `'f'` to something else and see the Debug panel change in live! This service is experimental but it has been really useful to me. **Javascript Lab** But you can also select the Javascript mode where your Javascript code will be immediately executed and also translated to Coffeescript! **Chocodown Lab** Literate programming... Chocodown panel lets you write Markdown, Chocokup and Coffeescript code that will be immediately translated to html and javascript! But more... when you display the **Dom** panel you can see immmediately the result! Basically, this panel is a Markdown editor, but you can insert code blocks by using the **#** and the **!** signs followed by the language you want to use: html, css, javascript, coffeescript, chocokup. When you use the # sign, Chocodown displays and highlights the following code. When you use the ! sign, it executes the code. And you can use both **#!** Copy the following code in the Chocokup Lab with the Dom panel: ### Here is a basic Chocodown sample: **Css code** #! css #chocodown-demo .header, #chocodown-demo .footer { border: 1px solid grey; background: maroon; color: white; text-align: center; } **Chocokup code** #! chocokup panel "#chocodown-demo", -> header -> 'header' footer -> 'footer' body -> for i in [0..3] button "#{i}" **Coffeescript code** #! coffeescript buttons = document.querySelectorAll '#chocodown-demo button' for button in buttons addEvent = button.addEventListener ? button.attachEvent addEvent.call button, "click", -> alert "I'm button #" + @innerhtml And see... Then change **[0.\.3]** to **[0.\.6]** and see the result... **Html Lab** But you can also select the Html mode where your Html code will be immediately rendered and also translated to Coffeekup/Chocokup! &nbsp; --- ## <a name="Choco-WriteModules"></a> How to write Modules [⌂](#Choco-Summary) You can create a module by pressing the `Create` button. It will create a module with the name you provide in the currently displayed folder. If you dont put a suffix the the filename, it will create a Coffescript file with .coffee suffix. Supported file types are: .coffee, .js, .html, .css, .md If you have asset files you want to be downloadable from the web (like images or js libraries), put them in the `/static` folder. It is supposed that you will put modules that run only in the **node.js** environment in the `/server` folder. Modules that run only in the browser will go int the `/client` folder, and modules that can run in both environment will be put in `/general` folder. If you put .coffee or .js files in the `/client` or `/general` folder, they will be compiled if .coffee and copied to the `/static/lib` folder and will be downloadable by your javascript client code (using the provided require function). If you create a general module (that can work on server and in browser), you will need to write something like the following code: class MyGeneralModule constructor: -> ... _module = window ? module _module.exports = MyGeneralModule The exported function `interface`, if present in your module, is used to return an [Interface.Web](#Choco-Interface-Web) object or an html content if someone calls that module with no parameter: i.e., in module `mymodule.coffee`: exports.interface = -> '<div>Hello</div>' or Interface = require 'chocolate/general/locco/interface' exports.interface = new Interface.Web -> div 'Hello' Will display `Hello` when called with `https://myserver/mymodule` You can also directly export an [Interface.Web](#Choco-Interface-Web) object Interface = require 'chocolate/general/locco/interface' module.exports = new Interface.Web -> div 'Hello' You can write module that runs on server with functions that you can call directly from the browser like this: exports.say_hello = (who = 'you', where = 'Paris') -> 'hello ' + who + ' in ' + where This function can be called like this: `https://myserver/mymodule?say_hello` and display `hello you in Paris` `https://myserver/mymodule?say_hello&me` and display `hello me in Paris` `https://myserver/mymodule?say_hello&me&London` and display `hello me in London` `https://myserver/mymodule?say_hello&where=Madrid` and display `hello you in Madrid` `https://myserver/mymodule?say_hello&who=me&where=Madrid` and display `hello me in Madrid` Those function can declare a system parameter `__` which contains: .appdir: application directory .datadir: application data directory .session: session object to store user's session data .request: HTTP request object .response: HTTP response object i.e.: exports.check_appdir = (__) -> "Application directory is:" + __.appdir Instead of a javascript function you can call a Locco Interface: Interface = require 'chocolate/general/locco/interface' exports.say_hello = new Interface defaults: who: 'you' where: 'Paris' render: ({who, where}) -> 'hello ' + who + ' in ' + where &nbsp; --- ## <a name="Late-DB"></a> LateDB [⌂](#Choco-Summary) `lateDB` provides you an in-memory javascript space that you can modify with an `update` method var lateDB = require('chocolate/general/latedb'); db = lateDB(); ### lateDB.update with one key, one data and one operation db.update('key': { op: func, data: some_data }); i.e. (in Coffeescript): db.update 'result': op: (data) -> (@log ?= []).push data data: "done" or in Javascript: db.update({ 'result': { op: function(data) { return (this.log != null ? this.log : this.log = []).push(data); }, data: "done" } }); will store in the database {result:{log:['done']}} `result` is the key parameter which defines a `section`/`table`/`bucket` name, in which you want to store some data It contains an `op` field which provides a function to execute on `this` location, and a `data` field which should contain the `data` to provide to the `op` function. What the `update` service do is that it records the `op` method and the `data` provided in a `log.db` file which will be reloaded and executed next time your app will be restarted. Your `op` and `data` should rather not produce object oriented data (using the prototyping chain), unless those objets provides a `stringify` method which should write a javascript code in the `log.db` file that will re-create the oject. ### lateDB.update (many key/data pairs and one operation) db.update({ 'key 1': 'data 1', 'key 2': 'data 2' }, func); i.e. (in Coffeescript): db.update 'key 1': 'data 1' 'key 2': 'data 2' , (data) -> for k,v of data then @[k] = v i.e (in Javascript): db.update({ 'key 1': 'data 1', 'key 2': 'data 2' }, function(data) { var k, v; for (k in data) { v = data[k]; this[k] = v; } }); will basicaly copy an object in the database {"key 1": "data 1", "key 2", "data 2"} ### lateDB.update (many key/operation pairs and one data) db.update({ data_key_1: 'data value 1', 'data_key_2': 'data value 2' }, {key_1: func_1, key_2:func_2}); i.e. (in Coffeescript): db.update {name:'doe', firstname:'john'}, 'UpperCase': (data) -> for k, v of data then @[k] = v.toString().toUpperCase() 'TwoLetters': (data) -> for k, v of data then @[k] = v.toString().substr(0,2) i.e (in Javascript): db.update({ name: 'doe', firstname: 'john' }, { 'UpperCase': function(data) { var k, v; for (k in data) { v = data[k]; this[k] = v.toString().toUpperCase(); } }, 'TwoLetters': function(data) { var k, v; for (k in data) { v = data[k]; this[k] = v.toString().substr(0, 2); } } }); will put an object in the database in two different places with two different functions { "UpperCase": {name:'DOE', firstname:'JOHN'}, "TwoLetters": {name:'do', firstname:'jo'} } And voilà, that's bascially all... Oh, there is one more thing... ## LateDB().tables LateDB provides relational-like services with insert, join and query capabilities: ### count tables db.tables.count() ### create table The table's name uses the plural form of the entity's name. You can specify both, separated by a slash `/`. If you just provide one name, it will be used as the table's name, it will be supposed to be of plural form, and it's last letter will be removed to form the corrsponding entity's name. In the following line, the table's name will be `categories` and the corresponding entity's name will be `category` db.tables.create 'categories/category' table = db "tables.categories" expect(table.entity_name).toBe('category') expect(table.alias).toBe('Category_') In the following line, the table's name will be `colors` and the corresponding entity's name will be `color` db.tables.create 'colors' table = db "tables.colors" expect(table.entity_name).toBe('color') expect(table.alias).toBe('Color_') db.tables.create 'brands' table = db "tables.brands" expect(table.entity_name).toBe('brand') db.tables.create 'cars' table = db "tables.cars" expect(table.entity_name).toBe('car') ### drop table You can delete a table if you no longer need it: db.tables.drop 'colors' ### insert data Primary key has to be called `id` db.tables.insert 'colors', id:1, name:'white' db.tables.insert 'colors', id:2, name:'black' db.tables.insert 'colors', id:3, name:'red' expect(db('tables.colors')[0].id).toBe 1 expect(db('tables.colors')[2].name).toBe 'red' db.tables.insert 'brands', id:1, name:'Mercedes' db.tables.insert 'brands', id:2, name:'BMW' db.tables.insert 'brands', id:3, name:'Toyota' db.tables.insert 'brands', id:4, name:'Honda' expect(db('tables.brands')[1].id).toBe 2 expect(db('tables.brands')[3].name).toBe 'Honda' Primary key `ìd` can be given by LateDB id = db.tables.id 'colors' db.tables.insert 'colors', id:id, name:'grey' expect(db('tables.colors').lines[id].name).toBe 'grey' Primary key `id` can be automatically given by LateDB db.tables.create 'colors', identity:on db.tables.insert 'colors', name:'grey' # id field is automatically inserted ### automatic index creation expect(db('tables.colors').index.id[1].id).toBe 1 expect(db('tables.colors').index.id[3].name).toBe 'red' ### update a line in a table db.tables.update 'brands', id:3, name:'Toyota Motors' expect(db('tables.brands').lines[3].name).toBe 'Toyota Motors' ### delete a line in a table db.tables.delete 'cars', id:7 expect(db('tables.cars').lines[7]).toBe undefined ### insert data with foreign keys Foreign keys' name have to end with `_id` and use the sigular version of the table's name which is it's corresponding entity's name. db.tables.insert 'cars', id:1, name:'SLK 200', color_id:1, brand_id:1 db.tables.insert 'cars', id:2, name:'SL 600', color_id:2, brand_id:1 db.tables.insert 'cars', id:3, name:'BMW Série 2 Cabriolet', color_id:2, brand_id:2 db.tables.insert 'cars', id:4, name:'BMW Série 3 Berline', color_id:3, brand_id:2 db.tables.insert 'cars', id:5, name:'Toyota Prius', color_id:1, brand_id:3 db.tables.insert 'cars', id:6, name:'Toyota Aygo', color_id:3, brand_id:3 db.tables.insert 'cars', id:7, name:'Honda Accord', color_id:2, brand_id:4 db.tables.insert 'cars', id:8, name:'Honda Jazz', color_id:1, brand_id:4 expect(db('tables.cars')[3].id).toBe 4 expect(db('tables.cars')[4].name).toBe 'Toyota Prius' expect(db('tables.cars')[7].color_id).toBe 1 ### query a table You can query using a straightforward syntax lines = db.tables.query select: 'colors.cars.brands' fields: ({cars, brands}) -> id: cars.id name: cars.name brand: brands.name params: color: 2, brand: 'Honda' where: colors: ({color}) -> @id is color brands: ({brand}) -> @name is brand expect(lines.length).toBe 1 expect(lines[0].brand).toBe 'Honda' or lines = db.tables.query 'colors.cars.brands' fields: ({cars, brands}) -> id: cars.id name: cars.name brand: brands.name params: color: 2, brand: 'Honda' where: colors: ({color}) -> @id is color brands: ({brand}) -> @name is brand You can directly query using the table's name and sort cars in reverse order lines = db.tables.query 'cars', sort: ['name':-1] expect(lines.length).toBe 8 expect(lines[4].name).toBe 'Honda Jazz' ### query a table and filter using a function lines = db.tables.query 'cars', (o) -> o.name is 'Honda Jazz' expect(lines.length).toBe 1 expect(lines[0].id).toBe 8 ### register a query to use it later You register a query by giving it a name and a definition. The name is composed by three parts: Entity-name\_Number-of-parameters\_Query-name The third part, the query name, is optional. If no table is specified in the query definition, the Entity_name is used to specify the table name. In the following example, we register a query on entity `Color` with no parameter and no definition. So it will retrieve all lines in `colors` table. db.tables.register 'Color_0':{} lines = db.tables.query 'Color' expect(lines.length).toBe 3 expect(lines[2].name).toBe 'red' ### register a query with a `filter` using an indexed foreign key field When you query a registered query, you have to provide the entity name, an array containing the paramters and an optional query name. lines = db.tables.query 'Car', [1], 'byColor' In the following example, we query the `cars` table with one parameter named `color` in the `keys` array which will receive the value `1`, and will target the `color_id` field in the `cars` table: db.tables.register 'Car_1_byColor': filter: keys: ['color'] clauses: ['color'] lines = db.tables.query 'Car', [1], 'byColor' expect(lines.length).toBe 3 expect(lines[1].name).toBe 'Toyota Prius' Specifying `color` in the `clauses` array tells the query service to look for the value `1` in the following fields wether they exist or not: - the indexed foreign key `color_id` - the indexed field `color` - the non indexed field `color` So you can query a non indexed field in the same way: db.tables.register 'Car_1_byName': filter: keys: ['name'] clauses: ['name'] lines = db.tables.query 'Car', ['SL 600'], 'byName' expect(lines.length).toBe 1 expect(lines[0].id).toBe 2 ### query directly without registration with `entity name` and `keys` You can provide directly the query definition instead of the name of a previously registered query: lines = db.tables.query 'Car', [2], filter: keys: ['brand'] clauses: ['brand'] expect(lines.length).toBe 2 expect(lines[1].name).toBe 'BMW Série 3 Berline' ### query and sort results lines = db.tables.query 'Car', sort: ['name'] expect(lines.length).toBe 8 expect(lines[4].name).toBe 'SL 600' ### query and sort cars in reverse order' lines = db.tables.query 'Car', sort: ['name':-1] expect(lines.length).toBe 8 expect(lines[4].name).toBe 'Honda Jazz' ### query with a join and sort cars on multiple fields Use the `select` clause in the query definition to define a join between tables. Simply put a dot between tables' name to define a n -> 1 or a 1 -> n relationship: cars.brands will join, by default, `cars` and `brands` on cars.brand_id = brands.id This will work if the `cars` table has a `brand_id` field. If the `cars` table has no `brand_id` field, it will try to look for a 1 -> n relationship (see below) You can also specify which field you want to take in each table. Simply put the fileds' name in parenthesis just after the table's name. If you don't specify fields' name, no field will be selected. To select all fields, put a star `*` If the same field name is selected in two different tables the second one will have prepended it's table's name and a dot. lines = db.tables.query select: 'cars(*).brands(name)' sort: ['brands.name', 'name':-1] expect(lines.length).toBe 8 expect(lines[4].name).toBe 'SLK 200' ### query by using an operator in the filter's clauses Currently, you can only use the `ìs` and `isnt` operators. lines = db.tables.query 'Car', [2], filter: keys: ['brand'] clauses:['brand', {field:'color_id', oper:'isnt', value:2}] expect(lines.length).toBe 1 expect(lines[0].name).toBe 'BMW Série 3 Berline' ### query by using a user defined function as a filter The filter function you provide will receive three parameters: the `line` to accept or not, the `keys` received as the query parameters and the line's table name. The function shoul return `true` if the line is accepted or `no` if it is rejected. lines = db.tables.query 'Car', filter: (line, keys, tableName) -> line.name.indexOf('SL') isnt 0 expect(lines.length).toBe 6 expect(lines[0].name).toBe 'BMW Série 2 Cabriolet' ### query with a join and select fields using a function When you want to define a 1 -> n join, you have to put the table on the right side of the relationship inside brackets `[]`. But you can omit it if no n -> 1 relationship exists between the two tables. In the following example colors.[cars] will join `colors` and `cars` on colors.id = cars.color_id In this example, no field is specified in the select clause, but the `map.add` clause defines a function that receives an `output` object and an `input` object. You just have to copy what you need from the `input` to the `output`. In the `input` object, every field is prefixed by its corresponding table's name. lines = db.tables.query 'Car', select: 'colors.[cars].brands' filter: clauses: [field:'colors.id', oper:'is', value:2] map: add: (o, i) -> o.id = i['cars.id'] o.name = i['cars.name'] o.brand = i['brands.name'] expect(lines.length).toBe 3 expect(lines[0].name).toBe 'SL 600' expect(lines[2].brand).toBe 'Honda' There is also a `map.remove` clause that can be used to remove fields from the selected ones (i.e. if you use the star `*` in the `select` clause. ### query with a join, filter unsing a where clause and select fields using a a fields clause This example is close to the preceeding one except it has a more straightforward syntax. The `fields` clause replaces the `map` one and the `where` clause replaces the `filter` one. The `where` clause does not (yet) use existing index on foreign keys. lines = db.tables.query select: 'colors.cars.brands' fields: ({cars, brands}) -> id: cars.id name: cars.name brand: brands.name where: colors: -> @id is 2 brands: -> @name is 'Honda' expect(lines.length).toBe 3 expect(lines[0].name).toBe 'SL 600' expect(lines[2].brand).toBe 'Honda' &nbsp; --- ## <a name="Choco-Dash"></a> Chocodash [⌂](#Choco-Summary) Chocodash is a small library that includes javascript utilities: ### <a name="Choco-Dash-Type"></a> _.Type, _.type [⌂](#Choco-Summary) `_.type` returns the type of an object _type({}) === '[object Object]' `_.Type` provides a Type enumeration _type({}) === _.Type.Object _.Type = Object: '[object Object]' Array: '[object Array]' Boolean: '[object Boolean]' Number: '[object Number]' Date: '[object Date]' Function: '[object Function]' Math: '[object Math]' String: '[object String]' Undefined: '[object Undefined]' Null: '[object Null]' ### <a name="Choco-Dash-Prototype"></a> _.prototype [⌂](#Choco-Summary) `_.prototype` makes it easy to create a Javascript prototype following the classical class way: *Coffeescript:* Service = _.prototype add: (a,b) -> a+b sub: (a,b) -> a-b *Javascript:* Service = _.prototype({ add: function(a, b) { return a + b; }, sub: function(a, b) { return a - b; } }); or the mixin way: *Coffeescript:* Service = _.prototype() Service.use -> @add = (a,b) -> a+b @sub = (a,b) -> a-b *Javascript:* Service = _.prototype(); Service.use(function() { this.add = function(a, b) { return a + b; }; return this.sub = function(a, b) { return a - b; }; }); Then use your prototype to create javascript objects: sevr = new Service(); expect(serv instanceof Service).toBe(true); expect(serv.add(1,1)).toBe(2); You can define a prototype initializer by using the `constructor` keyword: *Coffeescript:* Service = _.prototype constructor: (@name) -> serv = new Service "MyDoc" expect(serv.name).toBe "MyDoc" *Javascript:* Service = _.prototype({ constructor: function(name) { this.name = name; } }); serv = new Service("MyDoc"); expect(serv.name).toBe("MyDoc"); You can also create a prototype by adopting/copying another prototype's beahaviour and adding new functions: *Coffeescript:* MoreMath = -> @multiply = (a,b) -> a * b @divide = (a,b) -> a / b CopiedService = _.prototype adopt:Service, use:MoreMath cop = new CopiedService expect(cop.add 2,2).toBe 4 expect(cop.multiply 3,3).toBe 9 *Javascript:* MoreMath = function() { this.multiply = function(a, b) { return a * b; }; return this.divide = function(a, b) { return a / b; }; }; CopiedService = _.prototype({ adopt: Service, use: MoreMath }); cop = new CopiedService; expect(cop.add(2, 2)).toBe(4); expect(cop.multiply(3, 3)).toBe(9); You can finally create a prototype by inheriting another prototype's beahaviour and adding new functions that can access parent's overriden function: *Coffeescript:* InheritedService = _.prototype inherit:Service use: -> @sub = (a,b) -> a + ' - ' + b + ' = ' + _.super @, a,b inh = new InheritedService expect(inh.add 2,2).toBe 4 expect(inh.sub 2,2).toBe "2 - 2 = 0" *Javascript:* InheritedService = _.prototype({ inherit: Service, use: function() { return this.sub = function(a, b) { return a + ' - ' + b + ' = ' + _.super(this, a, b); }; } }); inh = new InheritedService; expect(inh.add(2, 2)).toBe(4); expect(inh.sub(2, 2)).toBe("2 - 2 = 0"); ### <a name="Choco-Dash-Defaults"></a> _.defaults [⌂](#Choco-Summary) `_.defaults` ensure default values are set on an object Set default values if not set: o = _.defaults({first:1}, {second:2}); expect(o.first).toBe(1); expect(o.second).toBe(2); Set default values on sub-object if not set and preserve other values: o = _.defaults({second:{sub1:'sub1'}}, {first:2, second:sub2:'sub2'}); expect(o.first).toBe(2); expect(o.second.sub1).toBe('sub1'); expect(o.second.sub2).toBe('sub2'); ### <a name="Choco-Dash-Reactive"></a> _.Signal, _.Observer, _.Publisher [⌂](#Choco-Summary) Here are Chocolate's **reactive** services. **_.signal** represents a value which can be observed Signals are objects representing observed values. They are read by executing the `value()` function with no arguments. They are set by executing the `value()` function with a signal definition as the only argument. a = new _.Signal(1); b = new _.Signal(function(){ a.value() }); expect(a.value()).toEqual(1); expect(b.value()).toEqual(1); a.value(2); expect(a.value()).toEqual(2); expect(b.value()).toEqual(2); **_.Observer** reports signal changes Observers are defined in a manner similar to Signals The primary differences of observers are: - they have no value to read - they cannot be observed themselves - they are notified only after signals have all been updated They are called upon Signal change: a = new _.Signal(1); b = null; c = new _.Observer(function(){ b = a.value() }); expect(b).toEqual(1); a.value(2); expect(b).toEqual(2); Together, Signals and Observers form a directed acyclic graph. Signals form the root and intermediate nodes of the graph, while Observers form the leaf nodes in the graph. When a signal is updated, it propagates its changes through the graph. Observers are updated last after all affected signals have been updated. From the perspective of observers, all signals are updated atomically and instantly . **_.Publisher** reports basic signal changes to one-to-many reporters. They use one internal pair of Signal and Observer asyncFunc = function() { var publisher = new _.Publisher; var callback = function() { return publisher.notify('done'); }; doAsyncStuff(callback); return publisher; }; asyncFunc().subscribe(function(answer) { // do something when notified }); ### <a name="Choco-Dash-Async"></a> _.async (or _.serialize ), _.parallelize [⌂](#Choco-Summary) Really simple tools to help manage asynchronous calls serialization. You can change this javascript code: db.createOrGetTable(function(table) { return table.insertRow(row, function() { return db.select(query(function(rows) { return console.log(rows.count); })); }); }); to this code: _.async(function(await, local) { await(function() { return db.createOrGetTable(function(table) { local.table = table; return this.next(); }); }); await(function(next) { return local.table.insertRow(row, function() { return this.next(); }); }); await(function(next) { return db.select(query(function(rows) { local.rows = rows; return this.next(); })); }); return await(function() { return console.log(local.rows.count); }); }); or in Coffeescript, this code: db.createOrGetTable (table) -> table.insertRow row, -> db.select query (rows) -> console.log rows.count to this code: _.async (await, local) -> await -> db.createOrGetTable (table) -> local.table = table; @next() await -> local.table.insertRow row, -> @next() await > db.select query (rows) -> local.rows = rows; @next() await -> console.log local.rows.count It helps you mix synchronous and asynchronous, iterative and recursive code, in a **simple** way with **no new concept** to learn. Here is an example taken from /general/chocodash spec file: var _, end, start, time1, time2, time3, aync_func; aync_func = function(duration, cb) { return setTimeout((function() { return cb(new Date().getTime()); }), duration); }; _ = require('chocolate/general/chocodash'); start = new Date().getTime(); time1 = time2 = time3 = end = null; _.serialize(function(defer) { defer(function(next) { return aync_func(250, function(time) { time1 = time; return next(); }); }); defer(function(next) { return aync_func(150, function(time) { time2 = time; return next(); }); }); defer(function(next) { return aync_func(350, function(time) { time3 = time; return next(); }); }); defer(function() { // expect(time1 - start).toBeGreaterThan(250 - 5); // expect(time2 - start).toBeGreaterThan(400 - 5); // expect(time3 - start).toBeGreaterThan(750 - 5); // expect(end - start).toBeLessThan(10); }); end = new Date().getTime(); }); ### <a name="Choco-Dash-Stringify"></a> _.stringify, _.parse [⌂](#Choco-Summary) `_.stringify` transforms a javascript object in a string that can be parsed back as an object You can stringify every property of an object, even a function or a Date: o = { u: void 0, n: null, i: 1, f: 1.11, s: '2', b: true, add: function(a, b) { return a + b; }, d: new Date("Sat Jan 01 2011 00:00:00 GMT+010