File Coverage

blib/lib/Cookieville.pm
Criterion Covered Total %
statement 39 39 100.0
branch 18 26 69.2
condition 2 6 33.3
subroutine 6 6 100.0
pod 2 2 100.0
total 67 79 84.8


line stmt bran cond sub pod time code
1             package Cookieville;
2              
3             =head1 NAME
4              
5             Cookieville - REST API for your database
6              
7             =head1 VERSION
8              
9             0.06
10              
11             =head1 DESCRIPTION
12              
13             L is a a web application which allow you to inspect and run
14             queries on your SQL database using a REST interface.
15              
16             This distribution also contain L for easy blocking and
17             non-blocking integration with the server.
18              
19             This application need a L based L to work.
20             It will query the result files on disk to gather meta information about the
21             schema instead of looking into the running database.
22              
23             THIS SERVER IS CURRENTLY EXPERIMENTAL AND WILL CHANGE WITHOUT ANY NOTICE.
24              
25             =head1 SYNOPSIS
26              
27             $ MOJO_CONFIG=/path/to/mojo.conf cookieville daemon --listen http://*:5000
28              
29             Example C:
30              
31             $ cat /path/to/mojo.conf
32             {
33             # config for cookieville
34             inactive_timeout => 10,
35             schema_class => "My::Schema",
36             connect_args => {
37             "DBI:mysql:database=some_database;host=localhost",
38             "dr_who",
39             "MostS3cretpassWord",
40             },
41              
42             # config for hypnotoad - https://metacpan.org/pod/Mojo::Server::Hypnotoad
43             hypnotoad => {
44             listen => [ "http://*:5000" ],
45             workers => 10,
46             },
47              
48             # will set up logging to a given file
49             log => {
50             path => "/path/to/cookieville.log",
51             level => "info", # or debug
52             },
53             }
54              
55             TIP: Give config file the file mode 0600 to protect your connect passwords.
56              
57             TIP: Run L with L and enough
58             workers in production. Reason for this is that L is blocking,
59             and therefor can only handle one database opartion pr. worker. You might
60             also want to tweak L to prevent a worker from running
61             a query for too long.
62              
63             =head1 RESOURCES
64              
65             =over 4
66              
67             =item * GET /
68              
69             Returns a description of this application:
70              
71             {
72             "version": "0.01",
73             "source": "https://github.com/jhthorsen/cookieville",
74             "resources": {
75             "schema_source_list": [ "GET", "/sources" ],
76             "schema_for_source": [ "GET", "/:source/schema" ],
77             "source_search": [ "GET", "/:source/search?q=:json&limit=:int&order_by:json" ],
78             "source_delete": [ "DELETE", "/:source/:id" ],
79             "source_patch": [ "PATCH", "/:source/:id" ],
80             "source_update_or_insert": [ "PUT", "/:source" ]
81             }
82             }
83              
84             The environment variable C can be used to limit the data returned:
85              
86             COOKIEVILLE_INFO=source,resources
87              
88             =item * GET /sources
89              
90             Returns a list of available sources (resultsets). Example:
91              
92             [ "Users", "Posts" ]
93              
94             =item * GET /:source/schema
95              
96             Returns the schema for the given C.
97              
98             =item * GET /:source/search
99              
100             Does a SELECT from the given C with a given set of query params:
101              
102             =over 4
103              
104             =item * q=:json (mandatory)
105              
106             C will be L and used as the
107             L.
108              
109             =item * columns=:json (optional)
110              
111             Only output the given columns. Example:
112              
113             columns=["id","name"]
114              
115             =item * limit=:int (optional)
116              
117             Used to limit the number of rows in the output.
118              
119             =item * page=:int (optional)
120              
121             Used for pagination when C is specified.
122              
123             =item * order_by=:json (optional)
124              
125             Sort the result by column(s). Examples:
126              
127             order_by={"-desc","name"}
128             order_by=["name","id"]
129              
130             =back
131              
132             The return value will be a JSON document containing the rows. Example:
133              
134             {
135             data: [
136             { "id": 1002, "name": "Jan Henning Thorsen", "age": 31 },
137             { "id": 3005, "name": "Billy West", "age": 62 }
138             ]
139             }
140              
141             TODO: Make sure integers from the database are actual integers in the
142             result JSON.
143              
144             The format L<.csv|http://en.wikipedia.org/wiki/Comma-separated_values> is
145             also supported. Example:
146              
147             GET /Users.csv?q={"age":31}&order_by=name
148              
149             =item * DELETE /:source/:id
150              
151             Used to DELETE a single record identified by C.
152              
153             The return value will be a JSON document with the number of rows deleted:
154              
155             {"n":1}
156              
157             NOTE: This will be C<{"n":0}> if the record was already deleted.
158              
159             =item * PATCH /:source/:id
160              
161             Used to do a (partial) UPDATE of a single row identified by C. The HTTP
162             body must be a JSON structure with the data to update to.
163              
164             The return value will have the new document. Example:
165              
166             {
167             "data": { "id": 1002, "name": "Jan Henning Thorsen", "age": 31 }
168             }
169              
170             Will return 404 if the given C does not match any records in the database.
171              
172             =item * PUT /:source
173              
174             Used to INSERT or UPDATE a single row. The HTTP body must be a JSON
175             structure with the data to insert or update.
176              
177             The return value will be a JSON document containing all the data for the
178             inserted or updated row. Example:
179              
180             {
181             "inserted": true, # or false
182             "data": { "id": 1002, "name": "Jan Henning Thorsen", "age": 31 }
183             }
184              
185             =back
186              
187             =head2 Error handling
188              
189             The API will return "200 OK" on success and another error code on failure:
190              
191             =over 4
192              
193             =item * 400
194              
195             Return the document below on invalid input data. C holds a
196             description of what is missing. Example:
197              
198             { "message": "Missing (q) query param." }
199              
200             =item * 401
201              
202             Return the document below on when not authorized. C holds a
203             description of why not. Example:
204              
205             { "message": "Invalid token." }
206              
207             =item * 404
208              
209             Return the document below if the given resource could not be found.
210             C holds a description of what is not found. Examples:
211              
212             { "message": "Resource not found" }
213             { "message": "No source by that name." }
214             { "message": "No matching records in database." }
215              
216             =item * 500
217              
218             { "message": "Internal server error." }
219              
220             Generic error when something awful happens. C might not make any
221             sense. Look at the server log for more details.
222              
223             =back
224              
225             Other error codes might be added in future releases.
226              
227             =head2 Queries
228              
229             The queries (referred to as the "q" query param in the API) are passed on as
230             the first argument to L.
231              
232             =cut
233              
234 10     10   1122676 use Mojo::Base 'Mojolicious';
  10         18  
  10         59  
235 10     10   2331498 use File::Spec ();
  10         21  
  10         6261  
236              
237             our $VERSION = '0.06';
238              
239             =head1 ATTRIBUTES
240              
241             =head2 inactive_timeout
242              
243             $int = $self->inactive_timeout;
244              
245             Used to set the number of seconds before a query agains the database time out.
246             Defaults to value from config, the environment variable
247             C or 10 seconds.
248              
249             =head2 connect_args
250              
251             $array_ref = $self->connect_args;
252              
253             Looks in L to find connection arguments for
254             L. See L for details.
255              
256             =head2 schema_class
257              
258             $class_name = $self->schema_class;
259              
260             Looks in L to find the schema class to use.
261             See L for details.
262              
263             =cut
264              
265             has inactive_timeout => sub {
266             $ENV{COOKIEVILLE_INACTIVE_TIMEOUT} || shift->config('inactive_timeout') || 10;
267             };
268              
269             has connect_args => sub {
270             shift->config('connect_args') || [];
271             };
272              
273             has schema_class => sub {
274             shift->config('schema_class') || '';
275             };
276              
277             =head1 HELPERS
278              
279             =head2 db
280              
281             $obj = $self->db;
282              
283             Returns an instance of L.
284              
285             =head1 METHODS
286              
287             =head2 setup_routes
288              
289             Used to setup the L.
290              
291             =cut
292              
293             sub setup_routes {
294 10     10 1 19 my $self = shift;
295 10         175 my $r = $self->routes;
296              
297 10 100       88 if (my $rules = $self->config('access_rules')) {
298 1         13 $self->plugin('Cookieville::Plugin::Authorize', $rules);
299 1         60 $r = $r->find('cookieville_authorizer');
300             }
301              
302 10 50       242 $self->routes->get('/')->to('schema#index')->name('cookieville') unless $r->find('cookieville');
303 10 50       4174 $r->get('/sources')->to('schema#sources_list')->name('schema_source_list') unless $r->find('schema_source_list');
304 10 50       5294 $r->get('/:source/schema')->to('schema#source_schema')->name('schema_for_source')
305             unless $r->find('schema_for_source');
306 10 50       5183 $r->get('/:source/search')->to('read#search')->name('source_search') unless $r->find('source_search');
307 10 50       5382 $r->delete('/:source/:id')->to('write#delete')->name('source_delete') unless $r->find('source_delete');
308 10 50       5588 $r->patch('/:source/:id')->to('write#patch')->name('source_patch') unless $r->find('source_patch');
309 10 50       5847 $r->put('/:source')->to('write#update_or_insert')->name('source_update_or_insert')
310             unless $r->find('source_update_or_insert');
311             }
312              
313             =head2 startup
314              
315             Will set up L and add L.
316              
317             =cut
318              
319             sub startup {
320 10     10 1 157486 my $self = shift;
321              
322 10 100       73 if ($ENV{MOJO_CONFIG}) {
323 3         9 $self->plugin('config');
324             }
325 10 100       6186 if (my $config = $self->config('log')) {
326 1   33     36 $config->{$_} and $self->log->$_($config->{$_}) for qw( level path );
327 1         96 delete $self->log->{handle};
328             }
329 10 100       142 if (my $config = $self->config('access_log')) {
330 2         28 $self->plugin('Cookieville::Plugin::AccessLog', $config);
331             }
332 10 100       340 if (my $schema_class = $self->schema_class) {
333 1 50       66 eval "require $schema_class;1" or die $@;
334             }
335              
336             $self->hook(
337             before_dispatch => sub {
338 58     58   1980417 my $c = shift;
339 58         1313 Mojo::IOLoop->stream($c->tx->connection)->timeout($self->inactive_timeout);
340             }
341 10         206 );
342              
343 10         269 push @{$self->renderer->classes}, __PACKAGE__;
  10         173  
344 10         384 $self->defaults(format => 'json', message => '');
345 10         450 $self->types->type(csv => 'text/csv');
346 10   33 59   996 $self->helper(db => sub { shift->stash->{db} ||= $self->schema_class->connect(@{$self->connect_args}); });
  59         31329  
  59         3051  
347 10         715 $self->setup_routes;
348             }
349              
350             =head1 COPYRIGHT AND LICENSE
351              
352             Copyright (C) 2014, Jan Henning Thorsen
353              
354             This program is free software, you can redistribute it and/or modify it under
355             the terms of the Artistic License version 2.0.
356              
357             =head1 AUTHOR
358              
359             Jan Henning Thorsen - C
360              
361             =cut
362              
363             1;
364              
365             __DATA__