📜 ⬆️ ⬇️

CouchApp: JavaScript applications in CouchDB

Once upon a time, when I practiced writing stored procedures, triggers, cursors under MSSQL, I was not bothered by the thought of an application where all business logic runs at the database level, and the presentation tier simply pulls the base and is responsible for drawing the results. A lot of my developer years have passed since then, but the opportunities for the implementation of this idea never met ... until I came across CouchDB.

I think that many have already heard about NoSQL databases, including Couch DB. Here I want to talk about the great opportunity to embed JavaScript applications in CouchDB, whose name is CouchApp.


')
CouchApp is described on the CouchDB book’s website as a “Javascript and HTML5 application that is sent directly to the browser from CouchDB.” I think that such a definition is not entirely accurate, since in this case HTML is sent to the browser, and the HTML that runs on the server decides which HTML to render. The scheme from the site illustrates this idea somewhat better:

Browser (UI and links between pages)
-------------- HTTP ---------------
CouchDB (persistence, business logic, and templating)

CouchDB is (or includes) a web server. It performs its function of delivering results in JSON-format to REST requests, which are the main way of communicating with the database in any operations (from CRUD operations on data to creating and deleting databases themselves). The data between the server and the client is sent over the HTTP protocol; accordingly, nothing prevents it from sending HTML. To do this, you just need to tell him about it in the right way.

An application in CouchDB starts with a “design document”. Since everything, including data, is called a document in CouchDB and is set in JSON format, the design of the document is no different. Of course, it should be specifically called and have a certain structure. The document itself contains the application code.

The simplest base document looks like this:
{
"_id" : "my_doc" ,
"_rev" : "1-7a8b01193c8798fa555243b354d1e9d7"
}

The design of the document should have _id in the form: _design/app_name . The application JavaScript code is spread over the fields of the JSON object defining the design of the document (with appropriate screening). It also indicates other application attributes, such as links to files (attachments), application manifest, etc. Deploying an application consists of loading the design of the document and file resources (attachments) into the database.

In order to be able to work normally on JS code in your favorite editor, not to collect bit by bit from the application design document folders, there is the couchapp utility. It is written in Python and is engaged in collecting application resources into one json document and sending it to the server, attachments are poured separately. It also allows you to generate an initial folder structure for the application, from which it will then collect resources.

So, the first thing to start with is to install the couchapp utility. I will not describe the installation process, it is well written in the wiki .

Then, to get acquainted with the base, you can either put the base yourself, or you can register free CouchDB hosting . The last option I took advantage of. Everything is fine in it (I will not say anything about the restrictions), except that it is impossible to look at the logs there, and it can be difficult to debug the application accordingly.

Now everything is ready to start practicing.

Start


I specifically wanted to write something more than “Hello world” and different from the blog that is developed in each tutorial, but at the same time similar to it, to be able to follow the examples in the documentation and not get too carried away with porting code from tutorial in the app. As practice shows, this allows you to immediately understand the nuances of dealing with a new technology.

The application that I will describe is the so-called flash cards. The principle is simple - in life, on one side of the card a word is written in the target language, on the other - its translation. In the application, the user, respectively, requires an interface to view the list of card sets (sets), the ability to edit the set, adding and changing words on the cards, and the interface of the card run itself.

The document set of cards in the database will be as follows:
{
"_id": "my set",
"_rev": "1-7a8b01193c8798fa555243b354d1e9d6",
"type": "set",
"cards": [
{
"front": "front_text",
"back": "back_text"
},
{
"front": "front_text second",
"back": "back_text second"
}
]
}

"_id" - a unique set identifier, which is also its name in combination (for simplicity)
"_rev" is the set revision number. When saving or creating a document, you can omit it, then it will be filled automatically.

The fields "_id" and "_rev" are mandatory attributes of any document.

“Type” is a type of document. Since there are no tables in CouchDB and all documents are in the database next to each other, then, for simple differentiation between samples, it is convenient to use a special field.
“Cards” is an array of objects that define cards. You can store each card separately in a document with the “card” type, but for a simple application I decided not to do this.

Create a page with a form for creating a new or editing an existing set.

Editing page


CouchApp has the concept of “show function”. This function is responsible for displaying the base document, i.e. transforms it into another structure, for example, can produce HTML on output.

Show functions are stored in the js file and must be located in the shows folder. The call to the “edit” function will occur when we contact http: //// _ design / flashcards / _show / edit / and http: //// _ design / flashcards / _show / edit / <document_id>.

The code of the file \ shows \ edit.js for our page:
function (doc, req) {
//!json shows._edit

var Mustache = require( "vendor/couchapp/lib/mustache" ),
path = require( "vendor/couchapp/lib/path" ).init(req);

var data = {
assets : path.asset(),
indexPath : path.list( 'sets' , 'all-sets' , {descending: true , limit:10})
};

data.doc = doc || {};

return Mustache.to_html(shows._edit, data);
}


The show function takes as the parameters the requested document, if it was requested and found, and an object that describes the incoming request. In the first case, the doc will be absent.

Next comes a comment, which is a directive to the couchapp script (called macro) to write the contents of the file \ shows \ _edit. * Into the variable “shows._edit”. This means that the folder can not contain more than one file named _edit, regardless of the extension. In our case, there will be a _edit.html file that contains the code of our page with the form. We return to it later.

Next comes the import of two modules. CouchDB follows the CommonJS convention for developing server-side javascript, so import functions may be familiar to Node.js developers. I did not work with Node.js and could not find any documentation in CouchDB by the available functions. I had to look into the CouchDB codes and figure out the examples.

The first module is Mustache, it is responsible for rendering everything. We need it to render the html page with the data that we will prepare for it. The second one is path, which allows us to generate links to other pages of our application and resources, such as js scripts for working with the database (some are generated by couchapp during initialization of the project, and some are already available on the web server of the database).

Next, a model object is created (from the MVC pattern). The assets field is the url prefix of the directory where the scripts are laid out, indexPath is the URL of the page with the list of our sets. The list of documents is displayed by list-functions, which will be discussed later. The .list method will return a reference to the sets function, to which, in turn, the result of the all-sets view function will be passed.

Below is the addition to the model of the document itself, i.e. set of cards. As I already wrote, we can access the edit function either with the document id or without it. If the document is found, it will be in the doc parameter. If it was not found, then we create an empty object for the set. Thus, when the set edit page opens, we edit either the new set or the existing set.

Part of the html-code of the page responsible for drawing the set:
< form id = "new-post" method = "post" >
{{#doc}}
< label > Set name < / label > < input type = "text" size = "20" name = "_id" value = "{{_id}}" >
< ul class = "cards" >
{{#cards}}
< li >
< label > Front < / label > < input type = "text" size = "20" name = "front" value = "{{front}}" >
< label > Back < / label > < input type = "text" size = "20" name = "back" value = "{{back}}" >
< / li >
{{/cards}}

< li >
< label > Front < / label > < input type = "text" size = "20" name = "front" >
< label > Back < / label > < input type = "text" size = "20" name = "back" >
< / li >
< / ul >

< button id = "add" > Add < / button >< br / >< br / >
{{/doc}}

< input type = "submit" value = "Save &rarr;" / >
< span id = "saved" style = "display:none;" > Saved < / span >
< br / >< br / >
< / form >

Nothing special. All in accordance with the mustache .

After we showed the form, now we need to write a functional for saving the form. To do this, you need to send a POST request with a JSON card set document. To do this, the following scripts are added to the page: /_utils/script/jquery.js and /_utils/script/jquery.couch.js. The first is jQuery, the second is a jQuery plugin that organizes requests to CouchDB.

Now our script:
( function ( $ ) {
$ ( '#new-post' ) . submit ( function ( ) {
var self = $ ( this ) , setName = $ ( 'input[name=_id]' , this ) . val ( ) ,
oldSet , newSet = { } , db = $. couch . db ( 'flashcards' ) ;

function saveDoc ( set ) {
db. saveDoc ( set ) , {
success : function ( resp ) {
window. location . reload ( true ) ;
}
} ) ;
}

//collect cards array
newSet. cards = $ ( 'ul.cards input' ) . toArray ( ) . reduce ( function ( arr , el , idx ) {
if ( ! ( idx % 2 ) ) { arr [ arr. length ] = { } ; }
arr [ arr. length - 1 ] [ ! ( idx % 2 ) ? "front" : "back" ] = el. value ;

return arr ;
} , [ ] ) ;

//retrieve old set if found or create new
db. openDoc ( setName , {
error : function ( code ) {
oldSet = { } ;
if ( setName. length ) {
oldSet._id = setName ;
}
saveDoc ( $. extend ( oldSet , newSet ) ;

} ,
success : function ( response ) {
oldSet = response ;
saveDoc ( $. extend ( oldSet , newSet ) ;
}
} ) ;

return false ;
} ) ;
} ) ( jQuery ) ;

The main operations on working with the database here are:
1. var db = $. couch . db ( 'flashcards' ) var db = $. couch . db ( 'flashcards' ) var db = $. couch . db ( 'flashcards' ) - getting a database object to call save, open methods, etc.
2. db. openDoc ( id , callback ) db. openDoc ( id , callback ) - gets the document from the database by id. Used to get the latest version of the document. You must send the document with the latest revision to the server, otherwise it will be rejected. CouchDB implements only this principle of sharing shared access to a resource. If the document is not found, then it is assumed that a new set is being created. Then it is called to save the entire set object to the base.
3. db. saveDoc ( doc , callback ) db. saveDoc ( doc , callback ) - save the document to the database. If everything is successful, then the page is reloaded. The case when something went wrong, and this can happen if an old revision was sent, I do not consider.

Now we have full functionality for creating and editing map sets. Let us turn to the display of the list of sets.

Display Lists


Before displaying the list, you need to make a selection. Sampling is done using view. Unlike SQL, where the query can be built dynamically and, then, work on the data in the table, the view works a little differently. It is not possible to pass any parameters from the HTTP request to the view, but they can be applied to the results obtained from the view.

View is a function applied to each document of the database. It is used to filter documents in order to impose an index on the results of this filtering. View consists of a map function and an optional reduce. To display all sets, the function \ views \ all-sets \ map.js (all-sets will be called view) looks like this:
function ( doc ) {
if ( doc. type === 'set' && doc. cards ) {
emit ( doc._id , null ) ;
}
}

Here it is checked for compliance of the document with the set type and the presence of an array with cards. In the case of a match, the emit function is called that takes two arguments, a key and a value. To display the set list, the value of the second parameter is not important, i.e. all you need is a key representing the name of the set. Results are also sorted by it. According to the obtained set of keys and indexes will be built.

You can make queries to view that narrow the selection. For some queries, a nontrivial key generation scheme is required; for others, the additional function reduce. I will not focus on this, as the documentation is pretty good written about it.

After we have a defined view, we can set the display of its results. If show functions were used to display documents, then list functions will be needed here. Function \ lists \ sets.js:
function ( head , req ) {
// !json lists._sets

var Mustache = require ( "vendor/couchapp/lib/mustache" ) ,
path = require ( "vendor/couchapp/lib/path" ) . init ( req ) ;

provides ( 'html' , function ( ) {
var data = {
assets : path. asset ( ) ,
createNewLink : path. show ( 'edit' )
} ;

data. sets = ( function ( ) {
var row , set , arr = [ ] ;
while ( row = getRow ( ) ) {
set = row. value ;
arr. push ( {
title : row. id ,
showView : path. show ( 'set' , row. id ) ,
editLink : path. show ( 'edit' , row. id ) ,
runsetLink : path. asset ( 'flashcards_zeptojs.html' ) + '#' + row. id
} ) ;
}
return arr ;
} ) ( ) ;

return Mustache. to_html ( lists._sets , data ) ;
} ) ;
}

A distinctive feature of the show-function is that the available function provides (content_type, callback), which calls the callback if the content-type of the request matches and getRow (), which returns the rows from the view. From which view to display the line is determined by url: http: //// _ design / flashcards / _list / sets / <view-name> ?. In the head parameter of the function, the view parameters will be written, in req - numerous parameters of the HTTP request. Otherwise, we do the same as in the show-function - we take a template and pass the resulting model there, and then return the result.

Two pages left behind the article with a preview of a set of cards and the interface of the run itself, but they are absolutely trivial and are implemented with the same set of functions described above.

This is probably all that I consider necessary to have an understanding about CouchApp. Then you can safely refer to the documentation and implement something meaningful.

Conclusion


What remains in the draft from the experience of studying CouchDB with CouchApp:

1. Very leaky documentation.
CouchDB Book, there are even two versions of them - 1.0 and draft, both are in the public domain, reveal in detail the basic concepts of the database itself, but the description of couchApp is very messy and with spaces. You have to go through different blog entries to get an overall picture of how things work. I even think that reading the whole book does not guarantee gaining a complete understanding, although it seems to me to read 300 pages to have a high-level understanding. On the other hand, there are small helloworld articles that only display a sacred inscription, but do not go deeper, which brings very little benefit.

The funny thing is the lack of a description of the JavaScript API in CouchApp. And I didn’t find a reference on such an API at all on couchone.com , nor on wiki.apache.org/couchdb , or on the official site couchapp.org .

The book CouchDB is invited to look at the example of Sofa (blog), which lies on github. The code of this application has gone so far that only in places you can find only a remote similarity. Plus, if the application code in the book uses enough basic things, then the application in the repository already uses higher-level constructs. When you figure out what's what, it's not so scary, but for a person who just discovered CouchApp, in my opinion, it's too difficult. Maybe this can be facilitated by having experience in server-side JS, I don’t know, I didn’t have that experience.

Also depressing the lack of documentation on the supplied javascript libraries. It is not clear where they lie (I did not immediately realize that some of the libraries are provided by the couchapp script, and some already exist on the server, but where it was still to be determined) what is their API and what are they doing. For example, in the CouchDB lead wife’s blog there is a description of test cases for javascript libraries (one plain js, the other jquery-based) performing ajax requests to the database. But these libraries are not used in the Sofa application. They use another library, which expands their capabilities, but which for some reason, on the first request, pulls out the entire design document of the application in order to build urls from it. And the size of the design document even in my small application is about 1 megabyte.

In general, judging by the documentation, then from the project it is struck with such a serious student diploma, but by no means a commercial product, despite the fact that the product is serious.

2. Security applications.
Initially, absolutely all users can make any queries to the database. This is prohibited by specifying database users. But since CouchApp spins within the same database, then if the user has access to CouchApp, then he has full access to all documents. And if it is possible to prohibit writing at the level of validation functions, then with reading this will not work. Any authorized user can make a built-in query / _all_docs and get all the documents in the database.

Probably, this can be bypassed by screwing over Apache, where you can close all unnecessary URLs, deciding at the same time the question of pretty urls (the user will not go to / _lists / sets / ...? ...), but I haven't tried it, although the same couchapp.org works that way.

From all this, I can conclude that it will be quieter to have CouchDB for some application that will take over security and POST requests from forms. But the project is developing and, if innovations that simplify life are written in imputed documentation, you can try to reconsider my views.

Thank you, I hope it was interesting.

Source: https://habr.com/ru/post/110675/


All Articles