diff --git a/examples/custom_store/README.md b/examples/custom_store/README.md new file mode 100644 index 0000000..0788434 --- /dev/null +++ b/examples/custom_store/README.md @@ -0,0 +1,124 @@ +# Custom Sumo Store Example + +This example is about creating a store in order to add some functionality. +We are going to use `PostgreSQL` as a backend and we want to store people (only with _name_ and _city_ fields). + +# Requirements +- `PostgreSQL` installed and a database created (in our code we called it _store_example_) + +## The Problem + +`SumoDB` provides us a great bunch of functions for CRUD operations but we need a special one. We would like +to have a `count_by_city/1` function which returns us the number of people from a given city. + +With the standard `SumoDB`API we could filter by city and then return the size of the array but we want to use the native power of `PostgreSQL`. + +## Creating our own store + +As we are using `PostgreSQL` we are going to use our `sumo_db_pgsql` adapter for sumo, you can find it [here](https://github.com/inaka/sumo_db_pgsql). +This Adapter provides us a store called `sumo_db_pgsql` which provides us the standard `SumoDB` API. We want to extend that module and add our custom `count_by_city/1` function. In order to achive that we create our new `our_pgsql_store` which gets all the functionality from `sumo_db_pgsql` using `mixer`. + +```erlang +-mixin([sumo_store_pgsql]). +``` + +Then we can create our function: + +```erlang +count_by_city(City, DocName, State) -> + #{conn := Conn} = State, + Query = [ + "SELECT Count(city) FROM ", escape(DocName), + " GROUP BY CITY ", + " HAVING ", + " city = $1" + + ], + parse_result(epgsql:equery(Conn, stringify(Query), [City]), State). +``` + +We are using `epgsql` directly from here because it is the driver we are using in the adapter. +Don't worry about `escape/1`, `stringify/1` and `parse_result/1`, you can check the real code in the [source](src/our_pgsql_store.erl). + +Now we have to use this new store on our config file. + +## Calling to our new function + +We have added the function to our store but now we have to call that function integrated with `SumoDB`, we are going to create a new function in `store_example` module: + +```erlang +count_by_city(City) -> + sumo:call(people, count_by_city, [City]). +``` + +This is how we call directly to a store. The first parameter is the model (defined in the config), the second one is the name of the store's function (our new created function) and the last one is a list with the parameters the that new function, in this case only one parameter. + +and thats all! + +## Running the example + +- First clone the code to your local. +- You must create a new database in `PostgreSQL`. +- Review the `config/sys.config` info about your database, by default the database configuration is: + +```erlang +{host, "127.0.0.1"}, +{port, 5432}, +{database, "store_example"}, +{username, "ferigis"}, +{password, "123456"} +``` +replace what you need +- Compile the code + +``` +rebar3 compile +``` + +- Run + +``` +erl -pa _build/default/lib/*/ebin -config config/sys.config -s store_example +``` + +## Play! + +Lets create some people: +```erlang +2> sumo:persist(people, person:new("Felipe", "San Jose")). +#{city => "San Jose",id => 1,name => "Felipe"} +3> sumo:persist(people, person:new("Carlos", "Cali")). +#{city => "Cali",id => 2,name => "Carlos"} +4> sumo:persist(people, person:new("Brujo", "Buenos Aires")). +#{city => "Buenos Aires",id => 3,name => "Brujo"} +5> sumo:persist(people, person:new("Marcos", "Buenos Aires")). +#{city => "Buenos Aires",id => 4,name => "Marcos"} +6> sumo:persist(people, person:new("Euen", "Buenos Aires")). +#{city => "Buenos Aires",id => 5,name => "Euen"} +``` + +Lets find one of them (here we are checking we have extended from the default PostgreSQL store correctly): + +```erlang +7> sumo:find_by(people, [{name, "Brujo"}]). +[#{city => <<"Buenos Aires">>,id => 3,name => <<"Brujo">>}] +``` + +Great! + +Now lets use our new function: + +```erlang +8> store_example:count_by_city("Buenos Aires"). +3 +9> store_example:count_by_city("San Jose"). +1 +10> store_example:count_by_city("Cali"). +1 +11> store_example:count_by_city("Namek"). +0 +``` + +It worked! + +This is the idea behind creating your own store diff --git a/examples/custom_store/config/sys.config b/examples/custom_store/config/sys.config new file mode 100644 index 0000000..5e8d25d --- /dev/null +++ b/examples/custom_store/config/sys.config @@ -0,0 +1,26 @@ +[ + {sumo_db, [ + {wpool_opts, [{overrun_warning, 100}]}, + {log_queries, true}, + {query_timeout, 30000}, + {storage_backends, [ + {our_backend_pgsql, sumo_backend_pgsql, [ + {host, "127.0.0.1"}, + {port, 5432}, + {database, "store_example"}, + {username, "ferigis"}, + {password, "123456"} + ]} + ]}, + {stores, [ + {pgsql_store, our_pgsql_store, [ + {storage_backend, our_backend_pgsql}, + {workers, 10} + ]} + ]}, + {docs, [ + {people, pgsql_store, #{module => person}} + ]}, + {events, []} + ]} +]. diff --git a/examples/custom_store/rebar.config b/examples/custom_store/rebar.config new file mode 100644 index 0000000..d5bf8e0 --- /dev/null +++ b/examples/custom_store/rebar.config @@ -0,0 +1,27 @@ +%% == Erlang Compiler == + +{erl_opts, [ + warn_unused_vars, + warn_export_all, + warn_shadow_vars, + warn_unused_import, + warn_unused_function, + warn_bif_clash, + warn_unused_record, + warn_deprecated_function, + warn_obsolete_guard, + strict_validation, + warn_export_vars, + warn_exported_vars, + warn_untyped_record, + warn_as_error, + debug_info, + {parse_transform, lager_transform} +]}. + +%% == Dependencies == + +{deps, [ + {mixer, "0.1.5", {pkg, inaka_mixer}}, + {sumo_db_pgsql, {git, "git://github.com/inaka/sumo_db_pgsql.git", {ref, "bc768fb"}}} +]}. diff --git a/examples/custom_store/src/our_pgsql_store.erl b/examples/custom_store/src/our_pgsql_store.erl new file mode 100644 index 0000000..d8db4c5 --- /dev/null +++ b/examples/custom_store/src/our_pgsql_store.erl @@ -0,0 +1,39 @@ +-module(our_pgsql_store). +-author("Felipe Ripoll "). + +-include_lib("epgsql/include/epgsql.hrl"). +-include_lib("mixer/include/mixer.hrl"). + +-mixin([sumo_store_pgsql]). % we want to mantain all the sumo_store_pgsql functions and adding ours + +-export([count_by_city/3]). + +count_by_city(City, DocName, State) -> + #{conn := Conn} = State, + Query = [ + "SELECT Count(city) FROM ", escape(DocName), + " GROUP BY CITY ", + " HAVING ", + " city = $1" + + ], + parse_result(epgsql:equery(Conn, stringify(Query), [City]), State). + +%%% Internal + +%% @private +escape(Name) when is_atom(Name) -> + ["\"", atom_to_list(Name), "\""]; +escape(String) -> + ["\"", String, "\""]. + +%% @private +stringify(Sql) -> binary_to_list(iolist_to_binary(Sql)). + +%% @private +parse_result({ok, _, []}, State) -> + {ok, {raw, 0}, State}; +parse_result({ok, _, [{Count}]}, State) -> + {ok, {raw, Count}, State}; +parse_result({error, Error}, State) -> + {error, Error, State}. diff --git a/examples/custom_store/src/person.erl b/examples/custom_store/src/person.erl new file mode 100644 index 0000000..e494e27 --- /dev/null +++ b/examples/custom_store/src/person.erl @@ -0,0 +1,22 @@ +-module(person). +-author("Felipe Ripoll "). + +-behaviour(sumo_doc). + +-export([sumo_schema/0, sumo_wakeup/1, sumo_sleep/1]). +-export([new/2]). + +new(Name, City) -> + #{name => Name, city => City}. + +sumo_schema() -> + Fields = [ + sumo:new_field(id, integer, [id, not_null, auto_increment]), + sumo:new_field(name, string, [not_null]), + sumo:new_field(city, string, [not_null]) + ], + sumo:new_schema(people, Fields). + +sumo_wakeup(Response) -> Response. + +sumo_sleep(Person) -> Person. diff --git a/examples/custom_store/src/store_example.app.src b/examples/custom_store/src/store_example.app.src new file mode 100644 index 0000000..9c05176 --- /dev/null +++ b/examples/custom_store/src/store_example.app.src @@ -0,0 +1,13 @@ +{application, store_example, [ + {description, "An example of creating your own store"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + sumo_db, + sumo_db_pgsql + ]}, + {mod, {store_example, []}}, + {modules, []} +]}. diff --git a/examples/custom_store/src/store_example.erl b/examples/custom_store/src/store_example.erl new file mode 100644 index 0000000..05133fc --- /dev/null +++ b/examples/custom_store/src/store_example.erl @@ -0,0 +1,21 @@ +-module(store_example). +-author("Felipe Ripoll "). + +-behaviour(application). + +-export([count_by_city/1]). +-export([start/0, start/2, stop/0, stop/1]). + +start() -> application:ensure_all_started(store_example). + +stop() -> application:stop(store_example). + +start(_Type, _Args) -> + ok = sumo:create_schema(), + store_example_sup:start_link(). + +stop(_State) -> + ok. + +count_by_city(City) -> + sumo:call(people, count_by_city, [City]). diff --git a/examples/custom_store/src/store_example_sup.erl b/examples/custom_store/src/store_example_sup.erl new file mode 100644 index 0000000..a86d910 --- /dev/null +++ b/examples/custom_store/src/store_example_sup.erl @@ -0,0 +1,20 @@ +-module(store_example_sup). +-author("Felipe Ripoll "). + +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, noarg). + +init(noarg) -> + SupFlags = #{ strategy => one_for_one + , intensity => 1000 + , period => 3600 + }, + + Children = [], + + {ok, {SupFlags, Children}}.