#!/usr/bin/env perl use strict; use Getopt::Long; use Pod::Usage; use File::Spec; use Path::Class qw( file dir ); use Plack::Runner; use Plack::App::RapidApp::rDbic; use RapidApp::Util qw(:all); # Special case - move cuddled '-I' arg from post to last so # that dbi arg logic can still work (as the second arg) push @ARGV, (shift @ARGV) if($ARGV[0] && $ARGV[0] =~ /^\-I\S+/); my $dsn; if($ARGV[0] && ! ($ARGV[0] =~ /^\-/) ) { if($ARGV[0] =~ /^dbi\:/) { # If the first argument is obviously a DBI dsn, use it $dsn = shift @ARGV; } elsif(-f $ARGV[0]) { # If the first argument is a path to a real file, assume it is SQLite $dsn = join(':','dbi','SQLite',shift @ARGV); } } sub _cleanup_exit { exit } END { &_cleanup_exit }; $SIG{$_} = \&_cleanup_exit for qw(INT KILL TERM HUP QUIT ABRT); my $schema_class; my $no_cleanup = 0; my $help = 0; my $name = 'rDbicServer'; my $crud_profile = 'editable'; my $total_counts_off = 0; my $tmpdir = dir( File::Spec->tmpdir ); my $port = 3500; my $run_webapi = 0; my $includes = []; my $metakeys; my $limit_tables_re = undef; my $limit_schemas_re = undef; my $exclude_tables_re = undef; my $exclude_schemas_re = undef; my $loader_options = []; my $connect_options = []; # From 'prove': Allow cuddling the paths with -I, -M and -e @ARGV = map { /^(-[IMe])(.+)/ ? ($1,$2) : $_ } @ARGV; GetOptions( 'help+' => \$help, 'dsn=s' => \$dsn, 'schema-class=s' => \$schema_class, 'port=i' => \$port, 'tmpdir=s' => \$tmpdir, 'no-cleanup+' => \$no_cleanup, 'app-class=s' => \$name, 'crud-profile=s' => \$crud_profile, 'total-counts-off+' => \$total_counts_off, 'run-webapi+' => \$run_webapi, 'I=s@' => $includes, 'metakeys=s' => \$metakeys, 'limit_tables_re|limit-tables-re=s' => \$limit_tables_re, 'limit_schemas_re|limit-schemas-re=s' => \$limit_schemas_re, 'exclude_tables_re|exclude-tables-re=s' => \$exclude_tables_re, 'exclude_schemas_re|exclude-schemas-re=s' => \$exclude_schemas_re, 'loader-option=s@' => \$loader_options, 'connect-option=s@' => \$connect_options ); pod2usage(1) if ($help || !$dsn); if (@$includes) { require lib; lib->import(@$includes); } { my $cnf = { app_namespace => $name, dsn => $dsn, tmpdir => $tmpdir, no_cleanup => $no_cleanup, crud_profile => $crud_profile, total_counts_off => $total_counts_off, isolate_app_tmp => 1, metakeys => $metakeys, limit_tables_re => $limit_tables_re, limit_schemas_re => $limit_schemas_re, exclude_tables_re => $exclude_tables_re, exclude_schemas_re => $exclude_schemas_re, loader_options => $loader_options, connect_options => $connect_options }; $cnf->{schema_class} = $schema_class if ($schema_class); my $App = Plack::App::RapidApp::rDbic->new( $cnf ); print "\n\n"; my $psgi = $run_webapi ? &_webapi_psgi($App) : $App->to_app; my $runner = Plack::Runner->new; $runner->parse_options('--port',$port); $runner->run($psgi); } sub _webapi_psgi { my $App = shift; print "Running WebAPI::DBIC::WebApp/HAL-Browser...\n"; use Plack::Builder; Module::Runtime::require_module('WebAPI::DBIC::WebApp'); Module::Runtime::require_module('Plack::App::File'); Module::Runtime::require_module('Alien::Web::HalBrowser'); my $hal_dir = Alien::Web::HalBrowser->dir; my $model = $App->model_class; Module::Runtime::require_module($model); my $connect_info = $model->config->{connect_info}; my $schema_class = $model->config->{schema_class}; Module::Runtime::require_module($schema_class); my $schema = $schema_class->connect( $connect_info->{dsn}, $connect_info->{user}, $connect_info->{password} ); my $app = WebAPI::DBIC::WebApp->new({ schema => $schema, writable => $crud_profile eq 'read-only' ? 0 : 1, http_auth_type => 'none' })->to_psgi_app; my $app_prefix = "/webapi-dbic"; my $plack = builder { enable "SimpleLogger"; # show on STDERR mount "$app_prefix/" => builder { mount "/browser" => Plack::App::File->new(root => "$hal_dir")->to_app; mount "/" => $app; }; # root redirect for discovery - redirect to API mount "/" => sub { [ 302, [ Location => "$app_prefix/" ], [ ] ] }; }; return $plack } 1; __END__ =head1 NAME rdbic.pl - Instant CRUD webapp for your database using RapidApp/Catalyst/DBIx::Class =head1 SYNOPSIS rdbic.pl DSN[,USER,PW] [options] rdbic.pl --dsn DSN[,USER,PW] [options] rdbic.pl SQLITE_DB [options] Options: --help Display this help screen and exit --dsn Valid DBI dsn connect string (+ ,user,pw) - REQUIRED --schema-class DBIC schema class name (blank/non-existant to auto-generate with Schema::Loader) --port Local TCP port to use for the test server (defaults to 3500) --tmpdir To use a different dir than is returned by File::Spec->tmpdir() --no-cleanup To leave auto-generated files on-disk after exit (in tmpdir) --app-class Name to use for the generated app (defaults to 'rDbicServer') --run-webapi EXPERIMENTAL: Run WebAPI::DBIC w/ HAL Browser instead of RapidApp --metakeys EXPERIMENTAL: Path to a RapidApp::Util::MetaKeys data file --loader-option DBIC::SL name=value opt(s), can be used more than once (same as -o in dbicdump) --connect-option name=value opt(s) to add to connect_info %extra_attributes --limit-tables-re Regex limits included table names (see also 'exclude' loader-option) --limit-schemas-re Regex limits RDBMS 'schema' names (see also 'db_schema' loader-option) --exclude-tables-re Regex string excludes table names --exclude-schemas-re Regex string excludes RDBMS 'schema' names --total-counts-off Initialize grids with total counts off (useful for very large tables) --crud-profile One of five choices to broadly control CRUD interface behavior (see below) -I Specifies Perl library include paths, like "perl"'s -I option. You may add multiple paths by using this option multiple times. CRUD Profiles: * editable Full CRUD is enabled with 'persist_immediately' turned off globally which means the user has to click "Save" to apply queued-up changes (DEFAULT) * edit-instant Full CRUD is enabled with 'persist_immediately' turned on. Changes are applied as soon as the cell is blurred after making a change * edit-gridadd Same as 'editable' except new rows are added directly to the grid instead of displaying an add record form * ed-inst-gridadd Same as 'edit-instant' except new rows are added directly to the grid; "Save" must still be clicked before the row is actually inserted * read-only No create/update/delete interfaces at all (rapidapp.pl default) Examples: rdbic.pl dbi:mysql:dbname,root,'' rdbic.pl to/any/sqlite_db_file rdbic.pl dbi:mysql:somedb,someusr,smepass --port 5005 --tmpdir /foo --no-cleanup rdbic.pl --dsn dbi:mysql:database=somedb,root,'' rdbic.pl --port 4001 --dsn dbi:SQLite:/path/to/sqlt.db rdbic.pl --dsn dbi:SQLite:/path/to/sqlt.db --tmpdir . --no-cleanup rdbic.pl my_sqlt.db --crud-profile=edit-gridadd rdbic.pl dbi:Pg:dbname=foo,usr,1234 --crud-profile=edit-instant rdbic.pl dbi:mysql:foo,root,'' --run-webapi rdbic.pl my_sqlt.db -Ilib --schema-class My::Existing::Schema rdbic.pl --dsn \ 'dbi:ODBC:Driver=TDS;TDS_Version=7.0;Server=10.1.2.3;Port=1433;Database=Blah',sa,topsecret \ --loader-option db_schema='{ Blah => "%" }' --loader-option use_moose=0 =head1 DESCRIPTION C is a handy utility which fires up a fully-functional RapidDbic/RapidApp application for a given database/DSN on-the-fly with a single shell command. This avoids having to bootstrap a real application with a name, config, directory, etc with L or L. All that needs to be supplied to C is a DSN, although additional options are also available. C can be used to replace tools like Navicat or PhpMyAdmin for a general-purpose database client. Internally, C simply bootstraps a new application using L in the same manner as L, but the new app is generated in a temporary directory and immediately launched using the standard L test server, all in one swoop. The generated/temporary files are automatically cleaned up on exit unless the C<--no-cleanup> option is supplied. You can also specify the location of the temporary directory with the C<--tmpdir> option (defaults to C or whatever is returned by File::Spec->tmpdir). If you combine with C<--no-cleanup> you can easily get the full working Catalyst/RapidApp app which was generated, for later use. For instance, these options will create and leave the generated app files within the current directory: --tmpdir . --no-cleanup A shorthand first argument syntax is also supported. If the first argument looks like a dsn (starts with 'dbi:') then it will be used as the dsn without having to supply C<--dsn> first. Additionally, if the first argument is a path to an existing regular file it is assumed to be an SQLite database file, and the appropriate dsn (i.e. "dbi:SQLite:$ARGV[0]") is used automatically. C is a wrapper around L which can be used directly in Plack-based setups and provides additional options and functionality not exposed in this script. See L for more information. The C script and L Plack App were also featured in the 2014 Catalyst Advent Calendar: =over =item * L =item * L =back =head1 SEE ALSO L, L, L =head1 SUPPORT IRC: Join #rapidapp on irc.perl.org. =head1 AUTHOR Henry Van Styn =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2014 by IntelliTree Solutions llc. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut