by Evan Miller

! this framework uses parameterized erlang modules !

if you hate parameterized modules, parse transforms, and dirty compiler hacks, stop reading immediately, because BossDB extensively (ab)uses all three in order to make database work productive and fun

btw: do you know difference between http server, web-framework and CMS? if you do this text is not to your attention at all

how it works? like Ruby-on-Rails if you want to know..., no no ... like Django



controllers


CB associates each URL with a function of a controller

each controller module should go into src/controller/ directory and the file name should start with the application name and end with _controller.erl, e.g. appname_some_controller.erl

the URL /foo/bar will call the function yourapp_foo_controller:bar/2/3

any other "helper functions" should go into src/lib/ directory

controllers can take one or two parameters:

after routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. for most conventional RESTful applications, the controller will receive the request, fetch or save data from/to a model and use a view to create HTML output. a controller can thus be thought of as a middle man between models and views. it makes the model data available to the view, and it saves or updates data from the user to the model

each exported controller function takes two or three arguments:

  1. the HTTP request method (as an atom 'GET'|'POST'|'PUT'|'DELETE')
  2. the list of slash-separated tokens after the "actionName" in the URI
  3. (optional) the result of a function named before_ in the controller

controller actions can return several values

to define a default action for a controller, simply add a default_action attribute to the controller like so:

      -default_action (index).  

after the controller action has returned, the next stage of processing is to generate a response. normally, the response will be generated from an ErlyDTL template WITH THE SAME NAME AS THE CONTROLLER ACTION

controller action can also redirect to another controller/action or to another URL or to some static file:

      bar ('GET', []) -> {redirect, "/static/somefile.html"}.


views


open up a new template src/view/greeting/list.html. btw, if you use Vim, use :setf htmldjango to get proper syntax highlighting

  <html> <head>
  <title> {% block title %} Greetings! {% endblock %} </title>
  </head> <body>

  {% block body %}

  <ul>
    {% if greetings %} 
      {% for x in greetings %}
          <li> {{ x.greeting_text }} </li>
      {% endfor %}
    {% else %}
          <li> No greetings! </li>
    {% endif %}
  </ul>

  {% endblock %}

  </body>
  </html>

in the code above, we've used a number of Django template tags that may be unfamiliar to you, but they should be easy to figure out

point your browser to http://localhost:8001/greeting/list to see the greeting you created in the console formatted in beautiful HTML

gotcha! there's nothing there


models


open a new file src/model/greeting.erl, and type this into it:

-module (greeting, [Id, GreetingText]).
-compile (export_all).

this is the model for "greeting" objects. it is really a parameterized module with two parameters. all models MUST have Id as their first parameter, but subsequent parameters can be whatever you like

this looks like an ordinary Erlang module, but it is harboring a number of secret superpowers. it is time for a little detour into the hidden wonders of the ChicagoBoss ORM, called BossRecord

BossRecords are specially compiled parameterized modules that follow the RoR ActiveRecord pattern. BossRecords go into your project's src/model/ folder and will have functions generated for saving them into the database and for accessing related BossRecords

important aspects of BossRecords:

now refresh the browser (so that this model file is properly compiled and loaded), then try this in the erlang shell:

    > G = greeting:new (id, "Hello, world!").
    {greeting,id,"Hello, world!"}

the new function just returns a new instance of greeting. you nedd to pass id as the first argument because that tells BossDB to generate a new ID number when the time comes; if you wanted a particular ID, you could specify it

the greeting instance is really just a tuple that includes

  • the module name
  • followed by all of the passed-in parameters
  • when you call a function of a parameterized module (other than new), the run-time system just binds values from that tuple to the module's parameter list before executing the function. passing around the greeting instance really is just passing around all of its parameter values. like other Erlang variables, these values are immutable, so we don't need to worry about locks, side effects, or other perils of OOP

        > G:greeting_text(). 
        "Hello, world!"
    "But wait just a gosh-darn minute," you might be thinking, "I don't remember putting a greeting_text/0 function in my greeting module! In fact, I don't remember putting ANY functions in there. Am I getting Alzheimer's? Where am I?"

    don't worry, you are not losing your mind

    before loading model modules into the system, the ChicagoBoss compiler surreptitiously attaches extra functions to them. here are a couple of others to try:

         > G:attributes().
         [{id,id},{greeting_text,"Hello, world!"}]
    
         > G:attribute_names().
         [id,greeting_text]
    
         > G:set(greeting_text, "Good-bye, world!").
         {greeting,id,"Good-bye, world!"}

    Erlang variables are immutable, so calling set/2 returns a new record without altering the old one in any way:

         > G.
         {greeting,id,"Hello, world!"}
    see? the G variable remains alive and full of hope

    but the most important function of them all, the function without which all the other functions would be useless and nothing except error messages would ever persist to disk, is this:

         > G:save().
         {ok,{greeting,"greeting-1","Hello, world!"}}

    congratulations! you just saved your first record to the database, and the id atom was replaced with a real-live identification string

    note that generated IDs in ChicagoBoss take the form "model-number" (here it is "greeting-1"). by including the model name in the ID, ChicagoBoss can guarantee that the IDs are unique across all of the different models

    you might be wondering how you are going to remember all of these useful functions that are clandestinely attached to each BossRecord. can you keep a secret? this is a secret you shouldn't even tell your spouse (particularly if s/he is a Rails programmer). open up a browser and visit http://localhost:8001/doc/greeting quick, close it before anyone sees. you just caught a glimpse of the ChicagoBoss's automatic EDoc, which will tell you about all the functions generated for each model. in the ChicagoBoss community we call this feature "/doc", for reasons you are left to ponder in private

    after saving the greeting, the G variable is still bound to the unsaved version:

    > G.
    {greeting,id,"Hello, world!"}

    oh dear. it would seem that the saved record is "lost" because we didn't bind anything to the return value of save/0. what is a budding CB programmer to do?

    not to worry - it's time to turn to BossRecord's best friend since childhood, the API with an attitude, Mr.BossDB


    BossDB is a library for querying the database


    to find the record that we saved, it is best to cast a wide net. we use boss_db:find/2:

          > boss_db:find(greeting, []).
          [{greeting,"greeting-1","Hello, world!"}]

    whew! the saved record hasn't gone anywhere - we can retrieve it anytime

    boss_db:find/2 is the simplest way to query the database; it takes a model name and a list of conditions and returns a list of results. that time we didn't want to impose any search conditions, so the list here is empty

    but try a few search conditions, just for fun:

          boss_db:find (greeting, [{id, 'equals', "greeting-1"}]). 
          boss_db:find (greeting, [{greeting_text, 'matches', "^Hell"}]). 
          boss_db:find (greeting, [{greeting_text, 'gt', "Hellespont"}]). 
          boss_db:find (greeting, [{greeting_text, 'lt', "Hellespont"}]).

    there are many more query operators available besides equals, matches, gt, and lt (some 18 in total). you will want to spend some time with the API documents to learn more about them

    of course, if we know the record ID, we don't need to mess with search conditions and query operators at all - we can use the very handy boss_db:find/1:

          > boss_db:find("greeting-1"). 
          {greeting,"greeting-1","Hello, world!"}
    

    the function returns either a record with the provided ID, or the atom 'undefined'. note that you didn't have to formally specify the name of the model, since it is embedded in the ID string

    creating a model from a JSON record

    you will probably have a case where you get a JSON or an API and you want to convert it to a model. the function boss_record:new_from_json/2 lets you do that automaticly

    to run this call new_from_json/2 with your model (atom) and your json record

    note that if you have in your json a field called 'id' you will have to rename or remove it!


    controllers, models and views all together - MCV


    you need to create a controller action to retrieve the greetings from the database

    open the controller src/controller/cb_tutorial_greeting_controller.erl and add a list action:

        list ('GET', []) -> 
        G = boss_db:find (greeting, []),
        {ok, [{greetings, G}]}.

    the first line queries the database for all greetings, and the second sends them all to the template. refresh the browser, and you'll see your HTML-formatted greeting at long last

    next we will create a form for submitting new greetings. add this to the bottom of the body block of src/view/greeting/list.html:

        <a href="{% url action="create" %}">New greeting...</a>

    here we've used a new tag, the url tag. it takes a key="value" argument list for constructing a URL, and must always have an action key

    then create a new template, src/view/greeting/create.html:

        {% extends "greeting/list.html" %}
        {% block title %} A new greeting! {% endblock %}
    
        {% block body %}
        <form method="post"> Enter a new greeting:
        <textarea name="greeting_text"> </textarea>
        <input type="submit">
        </form>
        {% endblock %}

    using the {% extends %} tag, this template inherits from the list.html template we created earlier. with it, the create.html template inserts its own content into the title and body blocks defined in list.html; in this way, you don't need to redefine the HTML <head>, <title> and <body> tags in every template. template inheritance is an essential strategy when creating and maintaining anything but a one-page website

    refresh the browser, and click the link to make sure that the form we created is there

    you need to process the form, so add a create action to the controller:

      create ('GET', []) ->
      ok;
      create ('POST', []) ->
      GreetingText = Req:post_param ("greeting_text"), 
      NewGreeting = greeting:new (id, GreetingText), 
      {ok, SavedGreeting} = NewGreeting:save (), 
      {redirect, [{action, "list"}]}.

    the first clause handles GET requests, and simply renders the template without an variables. the second clause handles POST requests

    it first pulls the "greeting_text" parameter from the form using Req:post_param/1. recall that Req is a parameter of the controller module (in fact, the only parameter). the action then creates a new greeting with the greeting text and saves it to the database. finally, the function returns a {redirect,Location} directive, which we have not encountered before

    this directive performs an HTTP 302 redirect to the specified location, which can be either a URL string or a proplist with an action key (just like the {% url %} tag in the template). in this example, redirect to the list action

    you now have a database-driven website

    next we will add a delete button to our application

    add the following form to the bottom of the body block of the list template src/view/greeting/list.html:

        <form method="post" action="{% url action="goodbye" %}">
        Delete: 
    
        <select name="greeting_id"> 
        {% for x in greetings %} 
        <option value="{{ x.id }}">{{ x.greeting_text }} 
        {% endfor %} 
        </select> 
    
        <input type="submit">
        </form>

    next, you need to process the form. add the following code to the bottom of the controller src/controller/cb_tutorial_greeting_controller.erl:

        goodbye('POST', []) -> 
        boss_db:delete(Req:post_param("greeting_id")), 
        {redirect, [{action, "list"}]}.

    the function boss_db:delete/1 takes a record ID and deletes the associated record from the database, just like that. use it with caution!

    simple validation

    to ensure that greetings are non-empty and tweetable, add the following code to the greeting model src/model/greeting.erl:

        validation_tests () -> 
        [ {fun () -> length (GreetingText) > 0 end, "Greeting must be non-empty!"}, 
        {fun () -> length (GreetingText) =< 140 end, "Greeting must be tweetable"}].

    validation_tests/0 is an optional function that should return a list of {TestFun, FailureMessage} tuples. if any of the functions returns false, the pending save operation is aborted, and the call to save/0 returns {error, FailureMessageList}

    try submitting an empty greeting. You should see (in erlang server shell) something like this:

    Error: {{{{cb_tutorial_greeting_controller,['Req']},create,2},
    {line,{17,25}}, {match,{error,["Greeting must be non-empty!"]}}},
    [{cb_tutorial_greeting_controller,create,3}, {boss_web_controller,execute_action,5}, 
    {boss_web_controller,process_request,5}, {timer,tc,3}, {boss_web_controller,handle_request,3}, 
    {mochiweb_http,headers,5}, {proc_lib,init_p_do_apply,3}]}

    following the error message, look at line 17 of the controller src/controller/cb_tutorial_greeting_controller.erl. that line is:

        {ok, SavedGreeting} = NewGreeting:save(),

    as indicated by the error message, we are getting a match error. the function is returning {error,["Greeting must be non-empty!"]}, but we are trying to match it to {ok, SavedGreeting}, so the process crashes (in the harmless, Erlang sense of the word "crash," of course)

    next, instead of crashing, we'll modify this part of the controller to match potential validation errors, and present them to the user in the template

    modify the controller accordingly:

        create ('POST', []) -> 
        GreetingText = Req:post_param ("greeting_text"), 
        NewGreeting = greeting:new (id, GreetingText), 
        case NewGreeting:save () of 
        {ok, SavedGreeting} -> 
        {redirect, [{action, "list"}]}; 
        {error, ErrorList} -> 
        {ok, [{errors, ErrorList}, 
        {new_msg, NewGreeting}]}
        end.

    if saving succeeds, we perform a redirect as before. but if validation fails, we send a list of errors to the template along with the greeting that failed to save

    now add an error clause at the the top of the body block in the view src/view/greeting/create.html to show any errors in red. also modify the textarea body as follows to display the greeting that was previously entered so that the user does not need to retype it:

        {% if errors %}
        {% for e in errors %} <br> <font color=red> {{ e }} </font> {% endfor %} 
        {% endif %}
    
        <form method="post">
        enter a new greeting:
        <textarea name="greeting_text">
        {% if new_msg %} {{ new_msg.greeting_text }} {% endif %}
        </textarea>
        <input type="submit">
        </form>

    note that the code inside the textarea tag should be on one line if you don't want the template to insert any spurious whitespace into the form

    now try entering in an exceedingly long greeting into the form. it can't be saved!


    backend db


    by default CB uses an in-memory database, which is useful for development and testing but not much else

    in production you wish to use backend database. you can. currently MongoDB, Tokyo, Mnesia, MySQL, PostgreSQL and Riak are supported

    * * *

    PostgreSQL / MySQL

    by default, SQL columns must be underscored versions of the attribute names, and SQL tables must be plural versions of the model names. (if the model is "puppy", the database table should be "puppies")

    you may want to override these defaults if you are working with an existing database. to specify your own column and table names, you can use the -columns() and -table() attributes in a model file like so:

    -module(puppy, [Id, Name]).
    -columns([{id, "puppy_id"}, {name, "puppy_name"}]).
    -table("puppy_table").

    open up boss.config and set db_adapter to `pgsql` or `mysql`. You should also set:

     - db_host
     - db_port
     - db_username
     - db_password
     - db_database

    set the db_port appropriately. By default, PostgreSQL listens on port 5432, MySql on port 3306

    to use CB with a SQL database, you need to create a table for each of your model files. the table name should be the plural of the model name. field names should be the underscored versions of the model attribute names. use whatever data types make sense for your application

    id

    the Id field of each model is assumed to be an integer supplied by the database (e.g., a SERIAL type in Postgres or AUTOINCREMENT in MySQL). specifying an Id value other than the atom 'id' for a new record will result in an error

    however, the generated BossRecord ID exposed to your application will still have the form "model_name-N"

    here are useful starting points for MySQL and PostgreSQL respectively:

        CREATE TABLE models (
            id MEDIUMINT NOT NULL AUTO_INCREMENT,
            ...
            PRIMARY KEY (id)
        );
    
        CREATE TABLE models (
            id SERIAL PRIMARY KEY,
            ...
        );

    counters

    to use the counter features, you need a table called "counters" with a "name" and "value" column, like these:

        CREATE TABLE counters (
            name                VARCHAR(255) PRIMARY KEY,
            value               INTEGER DEFAULT 0
        );

    * * *

    Tokyo Cabinet / Tokyo Tyrant

    1. download and install Tokyo Tyrant from here
    2. run Tyrant with a table database. other Tokyo database types are not supported !!!
    3. open up boss.config and set db_adapter to tyrant
    set the db_port appropriately. by default, Tokyo Tyrant listens on port 1978

    before starting CB start tokyo cabinet by command:

        $> ttserver path/to/your/db/file & 

    * * *

    MongoDB

    1. download and install MongoDB from here
    2. start MongoDB with `mongod`
    3. the MongoDB new version is not compatible with the authentication method used by the CB. as of MongoDB version 3, a new authentication method was introduced, SCRAM-SHA-1. this kicked the challenge-response (MONGODB-CR) mechanism from its default position. but you can edit auth schema

    to edit the authentication schema complete the following steps:

    • delete all users
      1. login as admin
      2. > use admin
      3. > db.system.users.remove({})
      OR
      1. > use DATABASE
      2. > db.runCommand({usersInfo: 1})
      3. > db.runCommand({dropUser: "USER"})
    • then
      1. > var schema = db.system.version.findOne({"_id" : "authSchema"})
      2. > schema.currentVersion = 3
      3. > db.system.version.save(schema)
    • recreate all deleted users
    4. open up boss.config and set the db_adapter to mongodb and db_database, db_passwrod and db_user to the names of your choice set the db_port appropriately. by default, MongoDB listens on port 27017

    note that in devel mode CB does not close its open connections to mongodb, so you can meet with resource exosted unless restart your webserver quite often

    * * *

    Mnesia

    1. the erlang VM needs a name and cookie to run Mnesia. you can set them with the 'vm_name' and 'vm_cookie' options in boss.config

    2. create a table for each model as well a table called '_ids_' with attributes for [type, id]. for more details see here

    > mnesia:create_schema ([node()]).
    ok
    > mnesia:start ().
    21:37:24.040 [info] Application mnesia started on node mynewproject@dog
    ok
    > mnesia:create_table ('_ids_', [{type, set}, {disc_copies, [node()]}, {attributes, [type, id]}]).
    {atomic,ok}
    > mnesia:create_table (greeting, [{type, set}, {disc_copies, [node()]}, {attributes, [id, text]}]).
    {atomic,ok}
    (mynewproject@dog)15> mnesia:stop ().
    stopped
    

    3. open up boss.config and set db_adapter to mnesia

    * * *

    Riak

    1. install Riak
    2. start a Riak node with `sudo riak start`
    3. open up boss.config and set db_adapter to riak

    for query operations to work, you first need to ensure that search is enabled in your Riak configuration:

       {riak_search, [{enabled, true}]}
    

    then run the following command in your Riak installation for each model:

       bin/search-cmd install 
    

    e.g. if you have a "greeting" model the command should be

       bin/search-cmd install greetings
    

    migrations


    CB supports Rails-like database migrations, which work like so:

        > boss_migrate:make (your_app_name, create_some_table).
    

    this creates a file in priv/migrations/ with a name like 1363463613_create_some_table.erl

    edit this file in order to do something along these lines:

        {create_some_table,
          fun(up) ->
    	      boss_db:execute("create table some_table ( id serial unique, name varchar,
    	                       birth_date timestamp, created_at timestamp )");
    	 (down) ->
    	      boss_db:execute("drop table some_table")
          end}.
    

    to actually run the migrations, you'd do

         > boss_migrate:run(gsd_web).
    

    you can also redo the last migration - which is useful when you're developing and perhaps haven't got the table definition quite right:

         > boss_migrate:redo(gsd_web, create_some_table).
    

    internally, migrations are tracked in a table called "schema_migrations" which is automatically created and managed by boss_db


    routes


    most routing takes place in the controller pattern-matching code

    but you can define additional routes in priv/my_application.routes file. the file contains a list of erlang terms, one per line finished with a dot. each term is a tuple with a URL or an HTTP status code as the first term, and a Location::proplist() as the second term

    the Location proplist must contain keys for controller and action. an optional application key will route requests across applications

    examples:

    {"/",                           [{controller, "world"}, {action, "list"}]}.
    {"/(book-[0-9,a-f]+)",          [{controller, "world"}, {action, "show"},  {id, '$1'}]}.
    {"/tag/(?<msg_tag>[0-9,a-z]+)", [{controller, "world"}, {action, "bytag"}, {tag, '$msg_tag'}]}.
    {404,                           [{controller, "world"}, {action, "lost"}]}.

    most routes directly render the specified action; however, routing across applications (as in the second example) results in a 302 redirect

    note that the {% url %} template tag will use the routes file to generate "pretty" URLs where appropriate

    additional Location parameters will be matched against the variable names of the controller's token list. For example, if user_controller.erl contains:

    profile('GET', [UserId]) -> ...

    then the location [{controller, "user"}, {action, "profile"}, {user_id, "123"}] will invoke the "profile" action of user_controller and pass in "123" as the only token (that is, UserId). if a location parameter does not match any variables in the token list, it will be passed in as a query parameter (e.g. ?user_id=123)

    routing URLs may contain regular expresssions. for example, the following route will match all URLs that start with a digit:

    {"/[0-9].*", [...]}.

    sub-expressions can be captured using parentheses and substituted with the atoms '$1', '$2', etc. for example, the following route will capture a string of digits and pass them in as the coupon_id parameter:

    {"/([0-9]+)", [{controller, "coupon"}, {action, "redeem"}, {coupon_id, '$1'}]}.

    alternatively, named groups may be used to identify captured sub-expressions. for example, the following route is equivalent to the one above:

    {"/(?<id>[0-9]+)", [{controller, "coupon"}, {action, "redeem"}, {coupon_id, '$id'}]}.

    the route expressions must match the entire URL. that is, the expressions are implicitly bookended with ^ and $

    the atoms which used as keys for parameters should map to the names of the controller' action parameters. the last must be in CamelStyle and the former must be in ant_style (where underscore points to capital letter)


    boss_db


    BossDB is a database abstraction layer used for querying and updating the database

    functions in the boss_db module include:

      find(Id::string()) -> Value | {error, Reason} 

    find a BossRecord with the specified Id (e.g. "employee-42") or a value described by a dot-separated path (e.g. "employee-42.manager.name")

      find(Type::atom(), Conditions, Options::proplist()) -> [BossRecord] 

    returns BossRecords of type Type matching all of the given Conditions. Options may include limit (maximum number of records to return), offset (number of records to skip), order_by (attribute to sort on), descending (whether to sort the values from highest to lowest), and include (list of belongs_to associations to pre-cache)

      find_first(Type::atom()) -> Record | undefined 

    query for the first BossRecord of type Type

      find_first(Type::atom(), Conditions) -> Record | undefined 

    query for the first BossRecord of type Type matching all of the given Conditions

      find_first(Type::atom(), Conditions, Sort::atom()) -> Record | undefined 

    query for the first BossRecord of type Type matching all of the given Conditions, sorted on the attribute Sort

      find_last(Type::atom()) -> Record | undefined 

    query for the last BossRecord of type Type

      find_last(Type::atom(), Conditions) -> Record | undefined 

    query for the last BossRecord of type Type matching all of the given Conditions

      find_last(Type::atom(), Conditions, TimeoutSort) -> Record | undefined 

    query for the last BossRecord of type Type matching all of the given Conditions

      count(Type::atom()) -> ::integer() 

    count the number of BossRecords of type Type in the database

      count(Type::atom(), TimeoutConditions) -> ::integer() 

    count the number of BossRecords of type Type in the database matching all of the given Conditions

      counter(Id::string()) -> ::integer() 

    treat the record associated with Id as a counter and return its value. Returns 0 if the record does not exist, so to reset a counter just use "delete"

      incr(Id::string()) -> ::integer() 

    treat the record associated with Id as a counter and atomically increment its value by 1

      incr(Id::string(), Increment::integer()) -> ::integer() 

    treat the record associated with Id as a counter and atomically increment its value by Increment

      delete(Id::string()) -> ok | {error, Reason} 

    delete the BossRecord with the given Id

      create_table(TableName::string(), TableDefinition) -> ok | {error, Reason} 

    create a table based on TableDefinition

      execute(Commands::iolist()) -> RetVal 

    execute raw database commands on SQL databases

      execute(Commands::iolist()span>, Params::list()) -> RetVal 

    execute database commands with interpolated parameters on SQL databases

      transaction(TransactionFun::function()) -> {atomic, Result} | {aborted, Reason} 

    execute a fun inside a transaction

      save_record(RecordBossRecord) -> {ok, SavedBossRecord} | {error, [ErrorMessages]} 

    save (that is, create or update) the given BossRecord in the database. Performs validation first; see validate_record/1

      validate_record(RecordBossRecord) -> ok | {error, [ErrorMessages]} 

    validate the given BossRecord without saving it in the database. ErrorMessages are generated from the list of tests returned by the BossRecord's validation_tests/0 function (if defined)
    the returned list should consist of {TestFunction, ErrorMessage} tuples, where TestFunction is a fun of arity 0 that returns true if the record is valid or false if it is invalid
    ErrorMessage should be a (constant) string which will be included in ErrorMessages if the TestFunction returns false on this particular BossRecord

      validate_record(RecordBossRecord, IsNew) -> ok | {error, [ErrorMessages]} 

    validate the given BossRecord without saving it in the database. ErrorMessages are generated from the list of tests returned by the BossRecord's validation_tests/1 function (if defined), where parameter is atom() on_create | on_update. The returned list should consist of {TestFunction, ErrorMessage} tuples, where TestFunction is a fun of arity 0 that returns true if the record is valid or false if it is invalid. ErrorMessage should be a (constant) string which will be included in ErrorMessages if the TestFunction returns false on this particular BossRecord

      validate_record_types(RecordBossRecord) -> ok | {error, [ErrorMessages]} 

    validate the parameter types of the given BossRecord without saving it to the database

      type(Id::string()) -> ::atom() 

    returns the type of the BossRecord with Id, or undefined if the record does not exist

    conditions and comparison operators

    the "find" and "count" functions each take a set of Conditions, which specify search criteria

    similar to Microsoft's LINQ, the Conditions can use a special non-Erlang syntax for conciseness. this special syntax can't be compiled with Erlang's default compiler, so you'll have to let Boss compile your controllers which use it

    Conditions looks like a list, but each element in the list uses a notation very similar to abstract mathematical notation with a left-hand side (an atom corresponding to a record attribute), a single-character operator, and a right-hand side (values to match to the attribute). the mathematical operators are not all ASCII!. as an alternative, you can also specify each condition with a 3-tuple with easier-to-type operator names

    example:

    to count the number of people younger than 25 with occupation listed as "student" or "unemployed", you would use:

     boss_db:count (person, [age < 25, occupation ∈ ["student", "unemployed"]]).

    this could also be written:

     boss_db:count (person, [{age, 'lt', 25}, {occupation, 'in', ["student", "unemployed"]}]).  

    valid conditions:

    key = Value
    {key, 'equals', Value}

    key ≠ Value
    {key, 'not_equals', Value}

    key ∈ [Value1, Value2, ...]
    {key, 'in', [Value1, Value2, ...]}

    the "key" attribute is equal to at least one element on the right-hand side

    key ∉ [Value1, Value2, ...]
    {key, 'not_in', [Value1, Value2, ...]}

    the "key" attribute is not equal to any element on the right-hand side

    key ∈ {Min, Max}
    {key, 'in', {Min, Max}}

    the "key" attribute is numerically between Min and Max

    key ∉ {Min, Max}
    {key, 'not_in', {Min, Max}}

    the "key" attribute is not between Min and Max

    key ∼ RegularExpression
    {key, 'matches', RegularExpression}

    the "key" attribute matches the RegularExpression. to perform a case-insensitive match, the expression should start with an asterisk (e.g. *erlang)

    key ≁ RegularExpression
    {key, 'not_matches', RegularExpression}

    the "key" attribute does not match the RegularExpression.

    key ∋ Token
    {key, 'contains', Token}

    the "key" attribute contains Token

    key ∌ Token
    {key, 'not_contains', Token}

    the "key" attribute does not contain Token

    key ⊇ [Token1, Token2, ...]
    {key, 'contains_all', [Token1, Token2, ...]}

    the "key" attribute contains all tokens on the right-hand side

    key ⊉ [Token1, Token2, ...]
    {key, 'not_contains_all', [Token1, Token2, ...]}

    the "key" attribute does not contain all tokens on the right-hand side

    key ∩ [Token1, Token2, ...]
    {key, 'contains_any', [Token1, Token2, ...]}

    the "key" attribute contains at least one of the tokens on the right-hand side

    key ⊥ [Token1, Token2, ...]
    {key, 'contains_none', [Token1, Token2, ...]}

    the "key" attribute contains none of the tokens on the right-hand side

    key > Value
    {key, 'gt', Value}

    key < Value
    {key, 'lt', Value}

    key ≥ Value
    {key, 'ge', Value}

    key ≤ Value
    {key, 'le', Value}


    eunits (by @jgordor)


    a CB application differences with a normal OTP app. when the application is fully compiled, it's behaves as a normal OTP app, but their internals have a few peculiarities (shortly):

    • compilation: controllers and models are special parametrized erlang modules, the compiler chain handles the magic and adds useful functions that helps application code reduction
    • directory structure: CB has a well defined directory structure to separate MVC and libs
    • multiapp setups: CB apps can be mounted as lego pieces, allowing a great separation and reusability of code into functional units running in the same erlang vm sharing logic & view code
    • boot: CB implements custom init modules located on priv/init that helps you run initialization code on the boot and shutdown process

    if you only need to test a plain erlang module (lib), the default rebar eunit implementation is enough. but if you need a complete test of a model, before you can run normal eunit tests you need the application fully initialized, boss_db & boss_news services started, all dependent apps initialized, ...

    simply put your eunit tests modules on src/tests/eunit and you can launch this from devel erlang shell

    example

    objective: test a model called "account" where we will store user account data. we will add some validation and custom functions plus a before_create/before_update model hook that will maintain the created_at and updated_at fields automatically

    the "account" model fields: Id, FirstName, LastName, CreatedAt and UpdatedAt

    database adapter: should work with all database adapters. for this "howto" we will use the internal mock database system shipped with the framework itself

    cd ChicagoBoss
    make
    make app PROJECT=ex_eunit
    cd ../ex_eunit
    

    now you have the project ready for development, time to add the account model, create a file named src/model/account.erl

    -module (account, [Id, 
                       FirstName::string(),
                       LastName::string(), 
                       CreatedAt::datetime(), 
                       UpdatedAt::datetime()
    ]).
    
    -compile (export_all).
    
    full_name () ->
      FirstName ++ " " ++ LastName.
    
    validation_tests () ->
      [
        {fun () -> FirstName =/= undefined andalso 
                   length (FirstName) =/= 0 end, 
                   {first_name, "Required"}},
        {fun () -> LastName =/= undefined andalso 
                   length (LastName) =/= 0 end, 
                   {last_name, "Required"}}
      ].
    
    before_create () ->
      Now = calendar:now_to_universal_time (erlang:timestamp()),
      {ok, set ([{created_at, Now}, {updated_at, Now}])}.
    
    before_update () ->
      {ok, set  (updated_at, calendar:now_to_universal_time (erlang:timestamp ()))}.
    

    the model code is complete, you can boot the cb app (./init-dev.sh) and start interacting from the erlang shell

    (ex_eunit@erlyhub-dev)1> boss_db:find(account, []). 
    []
    (ex_eunit@erlyhub-dev)2> A1 = boss_record:new(account, [{first_name, "Jose"}]).
    {account,id,"Jose",undefined,undefined,undefined}
    (ex_eunit@erlyhub-dev)3> A1:save().
    {error,[{last_name,"Required"}]}
    (ex_eunit@erlyhub-dev)4> A2 = A1:set(last_name, "Gordo").
    {account,id,"Jose","Gordo",undefined,undefined}
    (ex_eunit@erlyhub-dev)5> A2:save().
    {ok,{account,"account-1","Jose","Gordo",
                 {{2013,2,10},{17,2,9}},
                 {{2013,2,10},{17,2,9}}}}
    (ex_eunit@erlyhub-dev)6> boss_db:find(account, []).
    [{account,"account-1","Jose","Gordo",
              {{2013,2,10},{17,2,9}},
              {{2013,2,10},{17,2,9}}}]
    

    ok, seems that is working, now will make a complete eunit test suite, create a file named src/test/eunit/account_tests.erl

    -module (account_tests).
    -include_lib ("eunit/include/eunit.hrl").
    
    
    suite_test_ ()->
      Suite = {foreach, local, fun setup/0, tests ()},
      Suite.
    
    tests () ->
      [        
        {"Validations of first_name & last_name", ?_test (val_first_and_last_name ())},
        {"Validations of full_name", ?_test (val_full_name ())},
        {"Validations of generated_dates", ?_test (val_gen_dates ())}
      ].
    
    val_first_and_last_name () ->
      Account = boss_record:new (account, []),
      ?assertEqual ({error, [{first_name, "Required"}, {last_name, "Required"}]}, Account:save ()),
      Account2 = Account:set ([{first_name, "John"}, {last_name, "Dinho"}]),
      {Res, _} = Account2:save (),
      ?assertEqual (ok, Res).
    
    val_full_name () ->
      Account = boss_record:new (account, [{first_name, "John"}, {last_name, "Dinho"}]),
      ?assertEqual ("John Dinho", Account:full_name ()).
    
    val_gen_dates () ->
      Account = boss_record:new (account, [{first_name, "John"}, {last_name, "Dinho"}]),
      {ok, SavedAccount} = Account:save (),
      ?assertNotEqual (undefined, SavedAccount:created_at ()),
      ?assertNotEqual (undefined, SavedAccount:updated_at ()).
      
    setup () ->
      ok.
    

    and finally, start dev-mode and run the test!

    (ex_eunit@erlyhub-dev)> eunit:test(account).
      All 3 tests passed.
    ok
    

    that's all


    filters


    the functionality of CB can be extended with filter modules. filters are ideal for implementing site-wide or controller-specific features, such as authentication. filters can be chained together and reused, which makes them ideal for implementing and distributing plug-in features

    CB supports three kinds of filters:

    • before filters
    • middle filters
    • after filters

    a filter module can implement multiple kinds of filters

    before filters

    before filters can either transform an incoming request before it is handled by a controller action, or it can short-circuit the request processing and return an action return value itself. before filters are ideal for implementing authentication and authorization; the filter can attach the identity of the logged-in user to the incoming request, and immediately redirect unidentified or unauthorized users

    a before filter should export a function before_filter/2:

        before_filter (FilterConfig, RequestContext) -> {ok, RequestContext} | 
    

    the first argument is the filter's configuration value. this value can be defined in the configuration file and overridden by controllers on a per-request basis

    the second argument is the request context. the request context is a proplist with the following keys:

    • request
    • session_id
    • action
    • controller_module
    • tokens

    if the `before_filter` function returns `{ok, NewContext}`, the `NewContext` will be used for the rest of the request. You are free to modify values in the context or insert new values (e.g. `logged_in_user`)

    example:

    -module(my_before_filter)
    -export([before_filter/2])
    
    before_filter(_Config, RequestContext) ->
        IsAdmin = is_admin(RequestContext),
        {ok, [{is_admin, IsAdmin}|RequestContext]}.
    

    middle filters

    a middle filter transforms a controller return value to another controller return value. because controllers can return `{StatusCode, Payload, Headers}`, you can also use it to implement custom return values

    a middle filter should export a function middle_filter/3:

        middle_filter(ReturnValue, FilterConfig, RequestContext) -> NewReturnValue
    

    for example, if you wanted to implement a file handler {file, PathToFile}:

    -module(my_middle_filter).
    -export([middle_filter/3]).
    
    middle_filter({file, PathToFile}, _Config, _RequestContext) ->
        FileContents = read_file_somehow(PathToFile),
        FileMIMEType = figure_out_mime_type(PathToFile),
        {200, FileContents, [{"Content-Type", FileMIMEType}]};
    middle_filter(Other, _, _) -> Other.
    

    you might also use middle filters to insert commonly used values into the variable list before template rendering

    after filters

    an after filter transforms a `{StatusCode, Payload, Headers}` tuple just before a response is returned to the client:

        after_filter ({StatusCode, Payload, Headers}, FilterConfig, RequestContext) -> 
          {NewStatusCode, NewPayload, NewHeaders}
    

    you might use it to implement a custom compression or caching scheme

    filter installation

    filter module can be installed with the `controller_filter_modules` config option:
        {controller_filter_modules, [my_awesome_filter1, my_awesome_filter2]}
    

    filters are applied in order. for a particular controller, you can override the default filter list with the following three functions:

    -module (my_awesome_controller, [Req, SessionID]).
    -export ([before_filters/2, middle_filters/2, after_filters/2]).
    
    before_filters (DefaultFilters, RequestContext) -> BeforeFilters
    middle_filters (DefaultFilters, RequestContext) -> MiddleFilters
    after_filters (DefaultFilters, RequestContext) -> AfterFilters
    

    that way you can rearrange, insert, or delete filters based on the current request

    for now just put filter modules into your project's "lib" directory

    filter configuration

    the `FilterConfig` argument passed to the filter functions is set in your boss.config and can be overridden by the controllers

    to set a default config value for `my_awesome_filter` in your boss.config:

        {boss, [
            {controller_filter_config, [
                {my_awesome_filter, [{awesomeness, 100}]}
                ]}
            ]}
    

    then to override the value for a given request, export a `config/2` function from your controller:

    -module (my_cool_controller, [Req, SessionId]).
    -export ([config/2]).
    
    config (my_awesome_filter, _DefaultValue, RequestContext) -> [{awesomeness, 200}].
    

    the first argument is the name of the filter. It will either be the name of filter module itself or a short name provided by the filter. Short names can be shared by multiple filters, in which case they will receive the same config value

    setting a short name and default config value

    filter modules can export two functions to set a short name for themselves and to provide a default config value:

    -module (my_awesome_filter)
    -export ([config_key/0, config_default_value/0])
    
    config_key () -> 'awesome'.
    
    config_default_value () -> [{awesomeness, 50}].
    

    accessing the request context

    if a controller action function takes three arguments, the request context proplist will be passed in as the third argument

    build-in filters

    CB has four build-in filters:

    • boss_lang_filter
    • boss_cache_page_filter
    • boss_cache_vars_filter
    • boss_csrf_filter

    first three are enabled by default, CSRF verification filter is not enabled

    * templates * : you can use `{{ csrf_token }}` variable to display hidden input with csrf token in it

    * controllers * : `csrf_token` string is passed into controllers as part of `RequestContext` proplist (third parameter passed into controller)

    add `{do_not_enforce_csrf_checks, true}` tuple into filter config

    CSRF verification can be disabled application wide or on per controller basis