diff --git a/.env b/.env new file mode 100644 index 0000000..6e656f6 --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +SERVICE_NAME=dominant-v2 +OTP_VERSION=27.1 +REBAR_VERSION=3.23 +THRIFT_VERSION=0.14.2.3 +DATABASE_URL=postgresql://postgres:postgres@db/dmt diff --git a/.gitignore b/.gitignore index df53f7d..bcfd2f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,20 @@ -.rebar3 -_build -_checkouts -_vendor -.eunit -*.o -*.beam -*.plt -*.swp -*.swo -.erlang.cookie -ebin +# general log -erl_crash.dump -.rebar -logs -.idea -*.iml -rebar3.crashdump +/_build/ +/_checkouts/ *~ +erl_crash.dump +rebar3.crashdump +.tags* +*.sublime-workspace +.edts +.DS_Store +/.idea/ +*.beam +/test/log/ + +tags +.image.dev +bin + +.codestract diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b1ef63a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "psql-migration"] + path = psql-migration + url = git@github.com:valitydev/psql-migration.git + branch = get_rid_of_binary diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..6f2d4a4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 27.1 +rebar 3.23.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c3c1c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +ARG OTP_VERSION + +# Build the release +FROM docker.io/library/erlang:${OTP_VERSION} AS builder +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install thrift compiler +ARG THRIFT_VERSION +ARG TARGETARCH +RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${TARGETARCH}.tar.gz" \ + | tar -xvz -C /usr/local/bin/ + +# Hack ssh fetch and copy sources +ARG FETCH_TOKEN +RUN git config --global url."https://${FETCH_TOKEN}@github.com/".insteadOf ssh://git@github.com/ ;\ + mkdir /build +COPY . /build/ + +# Build the release +WORKDIR /build +RUN rebar3 compile && \ + rebar3 as prod release + +# Make a runner image +FROM docker.io/library/erlang:${OTP_VERSION}-slim + +ARG SERVICE_NAME +ARG USER_UID=1001 +ARG USER_GID=$USER_UID + +# Set env +ENV CHARSET=UTF-8 +ENV LANG=C.UTF-8 + +# Set runtime +WORKDIR /opt/${SERVICE_NAME} + +COPY --from=builder /build/_build/prod/rel/${SERVICE_NAME} /opt/${SERVICE_NAME} + +# Set up migration +COPY --from=builder /build/migrations /opt/${SERVICE_NAME}/migrations +COPY --from=builder /build/.env /opt/${SERVICE_NAME}/.env + +ENV WORK_DIR=/opt/${SERVICE_NAME} + +RUN echo "#!/bin/sh" >> /entrypoint.sh && \ + echo "exec /opt/${SERVICE_NAME}/bin/${SERVICE_NAME} foreground" >> /entrypoint.sh && \ + chmod +x /entrypoint.sh + +RUN groupadd --gid ${USER_GID} ${SERVICE_NAME} && \ + mkdir /var/log/${SERVICE_NAME} && \ + chown ${USER_UID}:${USER_GID} /var/log/${SERVICE_NAME} && \ + useradd --uid ${USER_UID} --gid ${USER_GID} -M ${SERVICE_NAME} +USER ${SERVICE_NAME} + +ENTRYPOINT [] +CMD ["/entrypoint.sh"] + +EXPOSE 8022 diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..c1ca7d0 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,17 @@ +ARG OTP_VERSION + +FROM docker.io/library/erlang:${OTP_VERSION} +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install thrift compiler +ARG THRIFT_VERSION +ARG TARGETARCH +RUN wget -q -O- "https://github.com/valitydev/thrift/releases/download/${THRIFT_VERSION}/thrift-${THRIFT_VERSION}-linux-${TARGETARCH}.tar.gz" \ + | tar -xvz -C /usr/local/bin/ + +# Set env +ENV CHARSET=UTF-8 +ENV LANG=C.UTF-8 + +# Set runtime +CMD ["/bin/bash"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6c0b767 --- /dev/null +++ b/Makefile @@ -0,0 +1,119 @@ +# HINT +# Use this file to override variables here. +# For example, to run with podman put `DOCKER=podman` there. +-include Makefile.env + +# NOTE +# Variables specified in `.env` file are used to pick and setup specific +# component versions, both when building a development image and when running +# CI workflows on GH Actions. This ensures that tasks run with `wc-` prefix +# (like `wc-dialyze`) are reproducible between local machine and CI runners. +DOTENV := $(shell grep -v '^\#' .env) + +# Development images +DEV_IMAGE_TAG = $(TEST_CONTAINER_NAME)-dev +DEV_IMAGE_ID = $(file < .image.dev) + +DOCKER ?= docker +DOCKERCOMPOSE ?= docker compose +DOCKERCOMPOSE_W_ENV = DEV_IMAGE_TAG=$(DEV_IMAGE_TAG) $(DOCKERCOMPOSE) -f compose.yaml -f compose.tracing.yaml +REBAR ?= rebar3 +TEST_CONTAINER_NAME ?= testrunner + +all: compile + +.PHONY: dev-image clean-dev-image wc-shell test + +dev-image: .image.dev + +get-submodules: + git submodule init + git submodule update + +.image.dev: get-submodules Dockerfile.dev .env + env $(DOTENV) $(DOCKERCOMPOSE_W_ENV) build $(TEST_CONTAINER_NAME) + $(DOCKER) image ls -q -f "reference=$(DEV_IMAGE_ID)" | head -n1 > $@ + +clean-dev-image: +ifneq ($(DEV_IMAGE_ID),) + $(DOCKER) image rm -f $(DEV_IMAGE_TAG) + rm .image.dev +endif + +DOCKER_WC_OPTIONS := -v $(PWD):$(PWD) --workdir $(PWD) +DOCKER_WC_EXTRA_OPTIONS ?= --rm +DOCKER_RUN = $(DOCKER) run -t $(DOCKER_WC_OPTIONS) $(DOCKER_WC_EXTRA_OPTIONS) + +DOCKERCOMPOSE_RUN = $(DOCKERCOMPOSE_W_ENV) run --rm $(DOCKER_WC_OPTIONS) + +# Utility tasks + +wc-shell: dev-image + $(DOCKER_RUN) --interactive --tty $(DEV_IMAGE_TAG) + +wc-%: dev-image + $(DOCKER_RUN) $(DEV_IMAGE_TAG) make $* + +wdeps-shell: dev-image + $(DOCKERCOMPOSE_RUN) $(TEST_CONTAINER_NAME) su; \ + $(DOCKERCOMPOSE_W_ENV) down + +wdeps-%: dev-image + $(DOCKERCOMPOSE_RUN) -T $(TEST_CONTAINER_NAME) make $(if $(MAKE_ARGS),$(MAKE_ARGS) $*,$*); \ + res=$$?; \ + $(DOCKERCOMPOSE_W_ENV) down; \ + exit $$res + +# Submodules tasks + +make_psql_migration: + make -C psql-migration/ + mkdir -p bin + mkdir -p migrations + cp ./psql-migration/_build/default/bin/psql_migration ./bin + +# Rebar tasks + +rebar-shell: + $(REBAR) shell + +compile: + $(REBAR) compile + +xref: + $(REBAR) xref + +lint: + $(REBAR) lint + +check-format: + $(REBAR) fmt -c + +dialyze: + $(REBAR) as test dialyzer + +release: + $(REBAR) as prod release + +eunit: + $(REBAR) eunit --cover + +common-test: + $(REBAR) ct --cover + +cover: + $(REBAR) covertool generate + +format: + $(REBAR) fmt -w + +clean: + $(REBAR) clean + +distclean: clean-build-image + rm -rf _build + +test: eunit common-test + +cover-report: + $(REBAR) cover diff --git a/README.md b/README.md index b029aea..96bd7b4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,31 @@ -dominant-v2 -===== +# dominant_v2 -An OTP application +## Migration -Build ------ +First compile migration script with - $ rebar3 compile +```shell +make wc-make_psql_migration +``` + +Then you can use script with + +```shell +bin/psql_migration -e .env +``` + +```shell +Usage: psql_migration [-h] [-d []] [-e []] + + -h, --help Print this help text + -d, --dir Migration folder [default: migrations] + -e, --env Environment file to search for DATABASE_URL [default: .env] + new Create a new migration + list List migrations indicating which have been applied + run Run all migrations + revert Revert the last migration + reset Resets your database by dropping the database in your + DATABASE_URL and then runs `setup` + setup Creates the database specified in your DATABASE_URL, and + runs any existing migrations. +``` diff --git a/apps/dmt/src/dmt.app.src b/apps/dmt/src/dmt.app.src new file mode 100644 index 0000000..a86d2ab --- /dev/null +++ b/apps/dmt/src/dmt.app.src @@ -0,0 +1,30 @@ +{application, dmt, [ + {description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {mod, {dmt_app, []}}, + {applications, [ + kernel, + stdlib, + damsel, + epg_connector, + epgsql, + epgsql_pool, + eql, + erl_health, + getopt, + envloader, + woody_user_identity, + jsx, + dmt_core, + dmt_object, + opentelemetry_api, + opentelemetry_exporter, + opentelemetry + ]}, + {env, []}, + {modules, []}, + + {licenses, ["Apache-2.0"]}, + {links, []} +]}. diff --git a/apps/dmt/src/dmt_api_woody_utils.erl b/apps/dmt/src/dmt_api_woody_utils.erl new file mode 100644 index 0000000..ce3fbd2 --- /dev/null +++ b/apps/dmt/src/dmt_api_woody_utils.erl @@ -0,0 +1,43 @@ +-module(dmt_api_woody_utils). + +-export([get_woody_client/1]). +-export([ensure_woody_deadline_set/2]). + +%% API + +-spec get_woody_client(atom()) -> woody_client:options(). +get_woody_client(Service) -> + Services = genlib_app:env(dmt_api, services, #{}), + make_woody_client(maps:get(Service, Services)). + +-spec make_woody_client(#{atom() => _}) -> woody_client:options(). +make_woody_client(#{url := Url} = Service) -> + lists:foldl( + fun(Opt, Acc) -> + case maps:get(Opt, Service, undefined) of + undefined -> Acc; + Value -> Acc#{Opt => Value} + end + end, + #{ + url => Url, + event_handler => get_woody_event_handlers() + }, + [ + transport_opts, + resolver_opts + ] + ). + +-spec get_woody_event_handlers() -> woody:ev_handlers(). +get_woody_event_handlers() -> + genlib_app:env(dmt_api, woody_event_handlers, [scoper_woody_event_handler]). + +-spec ensure_woody_deadline_set(woody_context:ctx(), woody_deadline:deadline()) -> woody_context:ctx(). +ensure_woody_deadline_set(WoodyContext, Default) -> + case woody_context:get_deadline(WoodyContext) of + undefined -> + woody_context:set_deadline(Default, WoodyContext); + _Other -> + WoodyContext + end. diff --git a/src/dominant-v2_app.erl b/apps/dmt/src/dmt_app.erl similarity index 75% rename from src/dominant-v2_app.erl rename to apps/dmt/src/dmt_app.erl index c4844ce..57b060c 100644 --- a/src/dominant-v2_app.erl +++ b/apps/dmt/src/dmt_app.erl @@ -1,16 +1,16 @@ %%%------------------------------------------------------------------- -%% @doc dominant-v2 public API +%% @doc dmt public API %% @end %%%------------------------------------------------------------------- --module(dominant-v2_app). +-module(dmt_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> - dominant-v2_sup:start_link(). + dmt_sup:start_link(). stop(_State) -> ok. diff --git a/apps/dmt/src/dmt_db_migration.erl b/apps/dmt/src/dmt_db_migration.erl new file mode 100644 index 0000000..6d611bf --- /dev/null +++ b/apps/dmt/src/dmt_db_migration.erl @@ -0,0 +1,332 @@ +-module(dmt_db_migration). + +%% API exports +-export([process/1]). + +%%==================================================================== +%% API functions +%%==================================================================== + +-define(OPTS, [ + {dir, $d, "dir", {string, "migrations"}, "Migration folder"}, + {env, $e, "env", {string, ".env"}, "Environment file to search for DATABASE_URL"} +]). + +-spec process(list()) -> ok | {error, any()}. +process(Args) -> + handle_command(getopt:parse(?OPTS, Args)). + +handle_command({error, Error}) -> + logger:error("~p", [Error]), + {error, Error}; +handle_command({ok, {Args, ["new", Name]}}) -> + Dir = target_dir(Args), + Timestamp = erlang:system_time(seconds), + MigrationName = [integer_to_list(Timestamp), "-", Name, ".sql"], + Filename = filename:join(Dir, MigrationName), + C = [ + "-- ", + Filename, + "\n", + "-- :up\n", + "-- Up migration\n\n", + "-- :down\n", + "-- Down migration\n" + ], + Result = + case file:write_file(Filename, list_to_binary(C), [exclusive]) of + ok -> {ok, "Created migration: ~s~n", [Filename]}; + {error, Reason} -> {error, "Migration can not be written to file ~s: ~s~n", [Filename, Reason]} + end, + handle_command_result(Result); +handle_command({ok, {Args, ["run"]}}) -> + Available = available_migrations(Args), + Result = with_connection( + Args, + fun(Conn) -> + Applied = applied_migrations(Conn), + ToApply = lists:filter(fun({Mig, _}) -> not lists:member(Mig, Applied) end, Available), + Results = apply_migrations(up, ToApply, Conn), + + report_migrations(up, Results) + end + ), + handle_command_result(Result); +handle_command({ok, {Args, ["revert"]}}) -> + Available = available_migrations(Args), + Result = with_connection( + Args, + fun(Conn) -> + case applied_migrations(Conn) of + [] -> + {error, "No applied migrations to revert"}; + Applied -> + LastApplied = lists:last(Applied), + case lists:keyfind(LastApplied, 1, Available) of + false -> + {error, "Migration ~p can not be found~n", [LastApplied]}; + Migration -> + Results = apply_migrations(down, [Migration], Conn), + report_migrations(down, Results) + end + end + end + ), + handle_command_result(Result); +handle_command({ok, {Args, ["reset"]}}) -> + {ok, Opts} = connection_opts(Args), + case maps:take(database, Opts) of + error -> + handle_command_result({error, "No database to reset~n"}); + {Database, Opts1} -> + case + with_connection( + Opts1#{database => "postgres"}, + fun(Conn) -> + if_ok(epgsql:equery(Conn, "drop database if exists " ++ Database)) + end + ) + of + ok -> + handle_command({ok, {Args, ["setup"]}}); + Other -> + handle_command_result(Other) + end + end; +handle_command({ok, {Args, ["setup"]}}) -> + {ok, Opts} = connection_opts(Args), + case maps:take(database, Opts) of + error -> + handle_command_result({error, "No database to set up~n"}); + {Database, Opts1} -> + case + with_connection( + Opts1#{database => "postgres"}, + fun(Conn) -> + case + epgsql:squery( + Conn, + "select 1 from pg_database where datname = '" ++ Database ++ "'" + ) + of + {ok, _, []} -> + if_ok(epgsql:squery(Conn, "create database " ++ Database)); + Other -> + if_ok(Other) + end + end + ) + of + ok -> + handle_command({ok, {Args, ["run"]}}); + Other -> + handle_command_result(Other) + end + end; +handle_command({ok, {_, _}}) -> + ok. + +%% Utils + +-type command_result() :: + ok + | {ok, io:format(), [term()]} + | {error, string()} + | {error, io:format(), [term()]}. + +-spec handle_command_result(command_result()) -> ok | {error, term()}. +handle_command_result(ok) -> + logger:info("All sql migrations completed"), + ok; +handle_command_result({ok, Fmt, Args}) -> + logger:info(Fmt, Args), + ok; +handle_command_result({error, Str}) -> + handle_command_result({error, Str, []}); +handle_command_result({error, Fmt, Args}) -> + logger:error(Fmt, Args), + {error, Fmt}. + +-spec with_connection(list() | map(), fun((pid()) -> command_result())) -> + command_result(). +with_connection(Args, Fun) -> + case open_connection(Args) of + {ok, Conn} -> + Fun(Conn); + {error, Error} -> + {error, "Failed to connect to database: ~p~n", [Error]} + end. + +connection_opts(Args) -> + connection_opts(Args, {env, "DATABASE_URL"}). + +connection_opts(Args, {env, URLName}) -> + maybe_load_dot_env(dot_env(Args)), + case os:getenv(URLName) of + false -> {error, "Missing DATABASE_URL.~n"}; + DatabaseUrl -> connection_opts(Args, {url, DatabaseUrl}) + end; +connection_opts(_Args, {url, DatabaseUrl}) -> + case uri_string:parse(string:trim(DatabaseUrl)) of + {error, Error, Term} -> + {error, {Error, Term}}; + Map = #{userinfo := UserPass, host := Host, path := Path} -> + {User, Pass} = + case string:split(UserPass, ":") of + [[]] -> {"postgres", ""}; + [U] -> {U, ""}; + [[], []] -> {"postgres", ""}; + [U, P] -> {U, P} + end, + + ConnectionOpts = #{ + port => maps:get(port, Map, 5432), + username => User, + password => Pass, + host => Host, + database => string:slice(Path, 1) + }, + + case maps:get(query, Map, []) of + [] -> + {ok, ConnectionOpts}; + "?" ++ QueryString -> + case uri_string:dissect_query(QueryString) of + [] -> + {ok, ConnectionOpts}; + QueryList -> + case proplists:get_value("ssl", QueryList) of + undefined -> {ok, ConnectionOpts}; + [] -> {ok, maps:put(ssl, true, ConnectionOpts)}; + Value -> {ok, maps:put(ssl, list_to_atom(Value), ConnectionOpts)} + end + end + end + end. + +-spec open_connection(list() | map()) -> {ok, epgsql:connection()} | {error, term()}. +open_connection(Args) when is_list(Args) -> + {ok, Opts} = connection_opts(Args), + open_connection(Opts); +open_connection(Opts) -> + epgsql:connect(Opts). + +target_dir(Args) -> + case lists:keyfind(dir, 1, Args) of + false -> + "."; + {dir, Dir} -> + _ = filelib:ensure_dir(Dir), + Dir + end. + +maybe_load_dot_env(DotEnv) -> + case filelib:is_file(DotEnv) of + true -> envloader:load(DotEnv); + % NB: Ignore the missing file. + false -> ok + end. + +dot_env(Args) -> + case lists:keyfind(env, 1, Args) of + false -> ".env"; + {env, DotEnv} -> DotEnv + end. + +-spec report_migrations(up | down, [{Version :: string(), ok | {error, term()}}]) -> ok. +report_migrations(_, L) when length(L) == 0 -> + logger:warning("No migrations were run"); +report_migrations(up, Results) -> + [logger:info("Applied ~s: ~p", [V, R]) || {V, R} <- Results], + ok; +report_migrations(down, Results) -> + [logger:info("Reverted ~s: ~p", [V, R]) || {V, R} <- Results], + ok. + +-define(DRIVER, epgsql). + +record_migration(up, Conn, V) -> + ?DRIVER:equery(Conn, "INSERT INTO __migrations (id) VALUES ($1)", [V]); +record_migration(down, Conn, V) -> + ?DRIVER:equery(Conn, "DELETE FROM __migrations WHERE id = $1", [V]). + +apply_migrations(Type, Migrations, Conn) -> + Results = lists:foldl( + fun + (_, [{_, {error, _}} | _] = Acc) -> + Acc; + (Migration = {Version, _}, Acc) -> + case apply_migration(Type, Migration, Conn) of + ok -> [{Version, ok} | Acc]; + {error, Error} -> [{Version, {error, Error}}] + end + end, + [], + Migrations + ), + lists:reverse(Results). + +apply_migration(Type, {Version, Migration}, Conn) -> + case eql:get_query(Type, Migration) of + {ok, Query} -> + case if_ok(?DRIVER:squery(Conn, Query)) of + ok -> + _ = record_migration(Type, Conn, Version), + ok; + {error, Error} -> + {error, Error} + end; + undefined -> + _ = record_migration(Type, Conn, Version), + ok + end. + +if_ok(Rs) when is_list(Rs) -> + Result = lists:map(fun(R) -> if_ok(R) end, Rs), + case lists:keyfind(error, 1, Result) of + false -> ok; + Error -> Error + end; +if_ok({ok, _}) -> + ok; +if_ok({ok, _, _}) -> + ok; +if_ok({ok, _, _, _}) -> + ok; +if_ok({error, {error, error, _, _, Descr, _}}) -> + {error, binary_to_list(Descr)}; +if_ok(Error) -> + {error, Error}. + +available_migrations(Args) -> + Dir = target_dir(Args), + Files = filelib:wildcard(filename:join(Dir, "*.sql")), + lists:map( + fun(Filename) -> + {ok, Migs} = eql:compile(Filename), + {filename:rootname(Filename), Migs} + end, + lists:usort(Files) + ). + +applied_migrations(Args) when is_list(Args) -> + with_connection( + Args, + fun(Conn) -> + applied_migrations(Conn) + end + ); +applied_migrations(Conn) when is_pid(Conn) -> + case ?DRIVER:squery(Conn, "SELECT id FROM __migrations ORDER by id ASC") of + {ok, _, Migs} -> + [binary_to_list(Mig) || {Mig} <- Migs]; + {error, {error, error, <<"42P01">>, _, _, _}} -> + %% init migrations and restart + {ok, _, _} = ?DRIVER:squery( + Conn, + "CREATE TABLE __migrations (" + "id VARCHAR(255) PRIMARY KEY," + "datetime TIMESTAMP DEFAULT CURRENT_TIMESTAMP)" + ), + applied_migrations(Conn) + end. diff --git a/apps/dmt/src/dmt_repository.erl b/apps/dmt/src/dmt_repository.erl new file mode 100644 index 0000000..7244cbf --- /dev/null +++ b/apps/dmt/src/dmt_repository.erl @@ -0,0 +1,654 @@ +-module(dmt_repository). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). + +%% API + +-export([commit/3]). +-export([get_object/2]). +-export([get_local_versions/3]). +-export([get_global_versions/2]). + +%% + +get_object({version, V}, ObjectRef) -> + case get_target_object(ObjectRef, V) of + {ok, #{ + global_version := GlobalVersion, + data := Data, + created_at := CreatedAt + }} -> + io:format("get_object Data ~p~n", [Data]), + {ok, #domain_conf_v2_VersionedObject{ + global_version = GlobalVersion, + %% TODO implement local versions + local_version = 0, + object = Data, + created_at = CreatedAt + }}; + {error, Reason} -> + {error, Reason} + end; +get_object({head, #domain_conf_v2_Head{}}, ObjectRef) -> + case get_latest_target_object(ObjectRef) of + {ok, #{ + global_version := GlobalVersion, + data := Data, + created_at := CreatedAt + }} -> + io:format("get_object head Data ~p~n", [Data]), + {ok, #domain_conf_v2_VersionedObject{ + global_version = GlobalVersion, + %% TODO implement local versions + local_version = 0, + object = Data, + created_at = CreatedAt + }}; + {error, Reason} -> + {error, Reason} + end. + +%% Retrieve local versions with pagination +get_local_versions(_Ref, _Limit, _ContinuationToken) -> + not_impl. + +%% Retrieve global versions with pagination +get_global_versions(_Limit, _ContinuationToken) -> + not_impl. + +assemble_operations(Commit) -> + try + lists:foldl( + fun assemble_operations_/2, + {[], #{}, []}, + Commit#domain_conf_v2_Commit.ops + ) + catch + {error, Error} -> + {error, Error} + end. + +assemble_operations_( + Operation, + {InsertsAcc, UpdatesAcc, UpdatedObjectsAcc} +) -> + case Operation of + {insert, #domain_conf_v2_InsertOp{} = InsertOp} -> + {ok, NewObject} = dmt_object:new_object(InsertOp), + #{ + tmp_id := TmpID, + references := Refers + } = NewObject, + + Updates1 = update_objects_added_refs({temporary, TmpID}, Refers, UpdatesAcc), + + io:format("~n {insert, #domain_conf_v2_InsertOp{} = InsertOp} ~p ~n", [Refers]), + + {[NewObject | InsertsAcc], Updates1, UpdatedObjectsAcc}; + {update, #domain_conf_v2_UpdateOp{targeted_ref = Ref} = UpdateOp} -> + case get_original_object_changes(UpdatesAcc, Ref) of +%% TODO Figure out how to stop several updates for the same object happening + Changes -> + {ok, ObjectUpdate} = dmt_object:update_object(UpdateOp, Changes), + io:format("~n {update, #domain_conf_v2_UpdateOp{targeted_ref = Ref} = UpdateOp} ~p ~n", [{Changes, ObjectUpdate}]), + UpdatesAcc1 = update_referenced_objects(Changes, ObjectUpdate, UpdatesAcc), + {InsertsAcc, UpdatesAcc1#{Ref => ObjectUpdate}, [Ref | UpdatedObjectsAcc]} + end; + {remove, #domain_conf_v2_RemoveOp{ref = Ref}} -> + #{ + references := OriginalReferences + } = OG = get_original_object_changes(UpdatesAcc, Ref), + UpdatesAcc1 = update_objects_removed_refs(Ref, OriginalReferences, UpdatesAcc), + + NewObjectState = dmt_object:remove_object(OG), + io:format("~n UpdatesAcc1#{Ref => NewObjectState} ~p ~n", [UpdatesAcc1#{Ref => NewObjectState}]), + {InsertsAcc, UpdatesAcc1#{Ref => NewObjectState}, [Ref | UpdatedObjectsAcc]} + end. + +update_referenced_objects(OriginalObjectChanges, ObjectChanges, Updates) -> + #{ + id := ObjectID, + references := OriginalReferences + } = OriginalObjectChanges, + #{references := NewVersionReferences} = ObjectChanges, + ORS = ordsets:from_list(OriginalReferences), + NVRS = ordsets:from_list(NewVersionReferences), + AddedRefs = ordsets:subtract(NVRS, ORS), + RemovedRefs = ordsets:subtract(ORS, NVRS), + + Updates1 = update_objects_added_refs(ObjectID, AddedRefs, Updates), + update_objects_removed_refs(ObjectID, RemovedRefs, Updates1). + +update_objects_added_refs(ObjectID, AddedRefs, Updates) -> + lists:foldl( + fun(Ref, Acc) -> + #{ + id := UpdatedObjectID, + referenced_by := RefdBy0 + } = OG = get_original_object_changes(Acc, Ref), + + Acc#{ + UpdatedObjectID => + OG#{ + referenced_by => [ObjectID | RefdBy0] + } + } + end, + Updates, + AddedRefs + ). + +update_objects_removed_refs(ObjectID, RemovedRefs, Updates) -> + lists:foldl( + fun(Ref, Acc) -> + #{ + id := UpdatedObjectID, + referenced_by := RefdBy0 + } = OG = get_original_object_changes(Acc, Ref), + RefdBy1 = ordsets:from_list(RefdBy0), + RefdBy2 = ordsets:del_element(ObjectID, RefdBy1), + Acc#{ + UpdatedObjectID => + OG#{ + referenced_by => ordsets:to_list(RefdBy2) + } + } + end, + Updates, + RemovedRefs + ). + +get_original_object_changes(Updates, Ref) -> + case Updates of + #{Ref := Object} -> + Object; + _ -> +%% {ok, #{ +%% id := ID, +%% type := Type, +%% referenced_by := RefdBy, +%% references := Refers, +%% data := Data +%% }} = get_latest_target_object(Ref), +%% %% NOTE this is done in order to decouple object type from object change type +%% #{ +%% id => ID, +%% type => Type, +%% referenced_by => RefdBy, +%% references => Refers, +%% data => Data +%% } + {ok, Res} = get_latest_target_object(Ref), + {Type, _} = Ref, + Res#{ + type => Type + } + end. + +%% NOTE Add new tables here +-define(TABLES, [ + category, + currency, + business_schedule, + calendar, + payment_method, + payout_method, + bank, + contract_template, + term_set_hierarchy, + payment_institution, + provider, + terminal, + inspector, + system_account_set, + external_account_set, + proxy, + globals, + cash_register_provider, + routing_rules, + bank_card_category, + criterion, + document_type, + payment_service, + payment_system, + bank_card_token_service, + mobile_op_user, + crypto_currency, + country, + trade_bloc, + identity_provider, + limit_config +]). + +commit(Version, Commit, CreatedBy) -> + {InsertObjects, UpdateObjects0, ChangedObjectIds} = assemble_operations(Commit), + + case + epgsql_pool:transaction( + default_pool, + fun(Worker) -> + ok = check_versions_sql(Worker, ChangedObjectIds, Version), + NewVersion = get_new_version(Worker, CreatedBy), + PermanentIDsMaps = insert_objects(Worker, InsertObjects, NewVersion), + UpdateObjects1 = replace_tmp_ids_in_updates(UpdateObjects0, PermanentIDsMaps), + ok = update_objects(Worker, UpdateObjects1, NewVersion), + {ok, NewVersion, maps:values(PermanentIDsMaps)} + end + ) + of + {ok, ResVersion, NewObjectsIDs} -> + NewObjects = lists:map( + fun(#{data := Data}) -> + Data + end, + get_target_objects(NewObjectsIDs, ResVersion) + ), + {ok, ResVersion, NewObjects}; + {error, {error, error, _, conflict_detected, Msg, _}} -> + {error, {conflict, Msg}}; + {error, Error} -> + {error, Error} + end. + +replace_tmp_ids_in_updates(UpdateObjects, PermanentIDsMaps) -> + maps:map( + fun(_ID, UpdateObject) -> + #{ + referenced_by := ReferencedBy + } = UpdateObject, + NewReferencedBy = lists:map( + fun(Ref) -> + case Ref of + {temporary, TmpID} -> + maps:get(TmpID, PermanentIDsMaps); + _ -> + Ref + end + end, + ReferencedBy + ), + UpdateObject#{ + referenced_by => NewReferencedBy + } + end, + UpdateObjects + ). + +check_versions_sql(Worker, ChangedObjectIds, Version) -> + lists:foreach( + fun({ChangedObjectType, ChangedObjectRef0} = ChangedObjectId) -> + io:format("ChangedObjectRef0 ~p~n", [ChangedObjectRef0]), + ChangedObjectRef1 = to_string(ChangedObjectRef0), + Query0 = + io_lib:format(""" + SELECT id, global_version + FROM ~p + WHERE id = $1 + ORDER BY global_version DESC + LIMIT 1 + """, [ChangedObjectType]), + case epgsql_pool:query(Worker, Query0, [ChangedObjectRef1]) of + {ok, _Columns, []} -> + throw({unknown_object_update, ChangedObjectId}); + {ok, _Columns, [{ChangedObjectRef, MostRecentVersion}]} when MostRecentVersion > Version -> + throw({object_update_too_old, {ChangedObjectRef, MostRecentVersion}}); + {ok, _Columns, [{_ChangedObjectRef, _MostRecentVersion}]} -> + ok; + {error, Reason} -> + throw({error, Reason}) + end + end, + ChangedObjectIds + ), + ok. + +get_new_version(Worker, CreatedBy) -> + Query1 = + """ + INSERT INTO GLOBAL_VERSION (CREATED_BY) + VALUES ($1::uuid) RETURNING version; + """, + case epgsql_pool:query(Worker, Query1, [CreatedBy]) of + {ok, 1, _Columns, [{NewVersion}]} -> + NewVersion; + {error, Reason} -> + throw({error, Reason}) + end. + +insert_objects(Worker, InsertObjects, Version) -> + lists:foldl( + fun(InsertObject, Acc) -> + #{ + tmp_id := TmpID, + type := Type, + forced_id := ForcedID, + references := References, + data := Data0 + } = InsertObject, + {ID, Sequence} = get_insert_object_id(Worker, ForcedID, Type), + Data1 = give_data_id(Data0, ID), + ID = insert_object(Worker, Type, ID, Sequence, Version, References, Data1), + Acc#{TmpID => {Type, ID}} + end, + #{}, + InsertObjects + ). + +insert_object(Worker, Type, ID0, Sequence, Version, References0, Data0) -> + ID1 = to_string(ID0), + Data1 = to_string(Data0), + References1 = lists:map(fun to_string/1, References0), + {Query, Params} = + case check_if_force_id_required(Worker, Type) of + true -> + Query0 = + io_lib:format(""" + INSERT INTO ~p (id, global_version, references_to, referenced_by, data, is_active) + VALUES ($1, $2, $3, $4, $5, TRUE); + """, [Type]), + Params0 = [ID1, Version, References1, [], Data1], + {Query0, Params0}; + false -> + Query1 = + io_lib:format(""" + INSERT INTO ~p (id, sequence, global_version, references_to, referenced_by, data, is_active) + VALUES ($1, $2, $3, $4, $5, $6, TRUE); + """, [Type]), + Params1 = [ID1, Sequence, Version, References1, [], Data1], + {Query1, Params1} + end, + case epgsql_pool:query(Worker, Query, Params) of + {ok, 1} -> + ID0; + {error, Reason} -> + throw({error, Reason}) + end. + +give_data_id({Tag, Data}, Ref) -> + {struct, union, DomainObjects} = dmsl_domain_thrift:struct_info('DomainObject'), + {value, {_, _, {_, _, {_, ObjectName}}, Tag, _}} = lists:search( + fun({_, _, _, T, _}) -> + case T of + Tag -> + true; + _ -> + false + end + end, + DomainObjects + ), + RecordName = dmsl_domain_thrift:record_name(ObjectName), + {_, _, [ + FirstField, + SecondField + ]} = dmsl_domain_thrift:struct_info(ObjectName), + First = get_object_field(FirstField, Data, Ref), + Second = get_object_field(SecondField, Data, Ref), + {Tag, {RecordName, First, Second}}. + +get_object_field({_, _, _, ref, _}, _Data, Ref) -> + Ref; +get_object_field({_, _, _, data, _}, Data, _Ref) -> + Data. + +update_objects(Worker, UpdateObjects, Version) -> + + io:format("~n update_objects UpdateObjects ~p~n", [UpdateObjects]), + maps:foreach( + fun({_, ID}, UpdateObject) -> + io:format("~n update_objects ID ~p~n", [ID]), + #{ + id := ID, + type := Type, + references := References, + referenced_by := ReferencedBy, + data := Data, + is_active := IsActive + } = UpdateObject, + ok = update_object(Worker, Type, ID, References, ReferencedBy, IsActive, Data, Version) + end, + UpdateObjects + ). + +update_object(Worker, Type, ID0, References0, ReferencedBy0, IsActive, Data0, Version) -> + Data1 = to_string(Data0), + ID1 = to_string(ID0), + References1 = lists:map(fun to_string/1, References0), + ReferencedBy1 = lists:map(fun to_string/1, ReferencedBy0), + Query = + io_lib:format(""" + INSERT INTO ~p + (id, global_version, references_to, referenced_by, data, is_active) + VALUES ($1, $2, $3, $4, $5, $6); + """, [Type]), + Params = [ID1, Version, References1, ReferencedBy1, Data1, IsActive], + case epgsql_pool:query(Worker, Query, Params) of + {ok, 1} -> + ok; + {error, Reason} -> + throw({error, Reason}) + end. + +get_insert_object_id(Worker, undefined, Type) -> + %% Check if sequence column exists in table + %% -- if it doesn't, then raise exception + case check_if_force_id_required(Worker, Type) of + true -> + throw({error, {object_type_requires_forced_id, Type}}); + false -> + {ok, LastSequenceInType} = get_last_sequence(Worker, Type), + case get_new_object_id(Worker, LastSequenceInType, Type) of + {undefined, Seq} -> + throw({error, {free_id_not_found, Seq, Type}}); + {NewID, NewSequence} -> + {NewID, NewSequence} + end + end; +get_insert_object_id(Worker, {Type, ForcedID}, Type) -> + case check_if_id_exists(Worker, ForcedID, Type) of + true -> + throw({error, {forced_id_exists, ForcedID}}); + false -> + {ForcedID, null} + end. + +check_if_force_id_required(Worker, Type) -> + Query = """ + SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 AND column_name = 'sequence'; + """, + case epgsql_pool:query(Worker, Query, [Type]) of + {ok, _Columns, []} -> + true; + {ok, _Columns, Rows} -> + lists:all( + fun(Row) -> + case Row of + {<<"sequence">>} -> + false; + _ -> + true + end + end, + Rows + ); + {error, Reason} -> + throw({error, Reason}) + end. + +get_last_sequence(Worker, Type) -> + Query = io_lib:format(""" + SELECT MAX(sequence) + FROM ~p; + """, [Type]), + case epgsql_pool:query(Worker, Query) of + {ok, _Columns, [{null}]} -> + {ok, 0}; + {ok, _Columns, [{LastID}]} -> + {ok, LastID}; + {error, Reason} -> + throw({error, Reason}) + end. + +get_new_object_id(Worker, LastSequenceInType, Type) -> + genlib_list:foldl_while( + fun(_I, {ID, Sequence}) -> + NextSequence = Sequence + 1, + NewID = dmt_object_id:get_numerical_object_id(Type, NextSequence), + case check_if_id_exists(Worker, NewID, Type) of + false -> + {halt, {NewID, NextSequence}}; + true -> + {cont, {ID, NextSequence}} + end + end, + {undefined, LastSequenceInType}, + lists:seq(1, 100) + ). + +check_if_id_exists(Worker, ID0, Type0) -> + %% Type1 = atom_to_list(Type0), + Query = io_lib:format(""" + SELECT id + FROM ~p + WHERE id = $1; + """, [Type0]), + ID1 = to_string(ID0), + case epgsql_pool:query(Worker, Query, [ID1]) of + {ok, _Columns, []} -> + false; + {ok, _Columns, [{ID1}]} -> + true; + {error, Reason} -> + throw({error, Reason}) + end. + +get_target_objects(Refs, Version) -> + get_target_objects(default_pool, Refs, Version). + +get_target_objects(Worker, Refs, Version) -> + lists:map( + fun(Ref) -> + {ok, Obj} = get_target_object(Worker, Ref, Version), + Obj + end, + Refs + ). + + +get_target_object(Ref, Version) -> + get_target_object(default_pool, Ref, Version). + +get_target_object(Worker, Ref, Version) -> + {Type, ID} = Ref, + ID0 = to_string(ID), + io:format("~n get_target_object ID ~p ID0 ~p and Version ~p~n", [ID, ID0, Version]), + Request = io_lib:format(""" + SELECT id, + global_version, + references_to, + referenced_by, + data, + is_active, + created_at + FROM ~p + WHERE id = $1 AND global_version <= $2 + ORDER BY global_version DESC + LIMIT 1 + """, [Type]), + case epgsql_pool:query(Worker, Request, [ID0, Version]) of + {ok, _Columns, []} -> + {error, {object_not_found, Ref, Version}}; + {ok, Columns, Rows} -> + io:format("get_target_object Res ~p ~n", [{Columns, Rows}]), + [Result | _] = to_marshalled_maps(Columns, Rows), + {ok, Result} + end. + +get_latest_target_object(Ref) -> + {Type, ID} = Ref, + ID0 = to_string(ID), + Request = io_lib:format(""" + SELECT id, + global_version, + references_to, + referenced_by, + data, + is_active, + created_at + FROM ~p + WHERE id = $1 + ORDER BY global_version DESC + LIMIT 1 + """, [Type]), + case epgsql_pool:query(default_pool, Request, [ID0]) of + {ok, _Columns, []} -> + {error, {object_not_found, Ref}}; + {ok, Columns, Rows} -> + [Result | _] = to_marshalled_maps(Columns, Rows), + {ok, Result} + end. + +to_marshalled_maps(Columns, Rows) -> + to_maps(Columns, Rows, fun marshall_object/1). + +to_maps(Columns, Rows, TransformRowFun) -> + ColNumbers = erlang:length(Columns), + Seq = lists:seq(1, ColNumbers), + lists:map( + fun(Row) -> + Data = lists:foldl( + fun(Pos, Acc) -> + #column{name = Field, type = Type} = lists:nth(Pos, Columns), + Acc#{Field => convert(Type, erlang:element(Pos, Row))} + end, + #{}, + Seq + ), + TransformRowFun(Data) + end, + Rows + ). + +%% for reference https://github.com/epgsql/epgsql#data-representation +convert(timestamp, Value) -> + datetime_to_binary(Value); +convert(timestamptz, Value) -> + datetime_to_binary(Value); +convert(_Type, Value) -> + Value. + +datetime_to_binary({Date, {Hour, Minute, Second}}) when is_float(Second) -> + datetime_to_binary({Date, {Hour, Minute, trunc(Second)}}); +datetime_to_binary(DateTime) -> + UnixTime = genlib_time:daytime_to_unixtime(DateTime), + genlib_rfc3339:format(UnixTime, second). + +marshall_object(#{ + <<"id">> := ID, + <<"global_version">> := Version, + <<"references_to">> := ReferencesTo, + <<"referenced_by">> := ReferencedBy, + <<"data">> := Data, + <<"created_at">> := CreatedAt, + <<"is_active">> := IsActive +}) -> + dmt_object:just_object( + from_string(ID), + Version, + lists:map(fun from_string/1, ReferencesTo), + lists:map(fun from_string/1, ReferencedBy), + from_string(Data), + CreatedAt, + IsActive + ). + +to_string(A0) -> + A1 = term_to_binary(A0), + base64:encode_to_string(A1). + +from_string(B0) -> + B1 = base64:decode(B0), + binary_to_term(B1). diff --git a/apps/dmt/src/dmt_repository_client_handler.erl b/apps/dmt/src/dmt_repository_client_handler.erl new file mode 100644 index 0000000..09e0252 --- /dev/null +++ b/apps/dmt/src/dmt_repository_client_handler.erl @@ -0,0 +1,59 @@ +-module(dmt_repository_client_handler). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +-export([handle_function/4]). + +handle_function(Function, Args, WoodyContext0, Options) -> + DefaultDeadline = woody_deadline:from_timeout(default_handling_timeout(Options)), + WoodyContext = dmt_api_woody_utils:ensure_woody_deadline_set(WoodyContext0, DefaultDeadline), + do_handle_function(Function, Args, WoodyContext, Options). + +default_handling_timeout(#{default_handling_timeout := Timeout}) -> + Timeout. + +do_handle_function('CheckoutObject', {VersionRef, ObjectRef}, _Context, _Options) -> + %% Fetch the object based on VersionReference and Reference + case dmt_repository:get_object(VersionRef, ObjectRef) of + {ok, Object} -> + {ok, Object}; + {error, global_version_not_found} -> + woody_error:raise(business, #domain_conf_v2_GlobalVersionNotFound{}); + {error, object_not_found} -> + woody_error:raise(business, #domain_conf_v2_ObjectNotFound{}); + {error, Reason} -> + woody_error:raise(system, {internal, Reason}) + end; +do_handle_function('GetLocalVersions', {Request}, _Context, _Options) -> + #domain_conf_v2_GetLocalVersionsRequest{ + ref = Ref, + limit = Limit, + continuation_token = ContinuationToken + } = Request, + %% Retrieve local versions with pagination + case dmt_repository:get_local_versions(Ref, Limit, ContinuationToken) of + {ok, Versions, NewToken} -> + {ok, #domain_conf_v2_GetVersionsResponse{ + result = Versions, + continuation_token = NewToken + }}; + {error, object_not_found} -> + woody_error:raise(business, #domain_conf_v2_ObjectNotFound{}); + {error, Reason} -> + woody_error:raise(system, {internal, Reason}) + end; +do_handle_function('GetGlobalVersions', {Request}, _Context, _Options) -> + #domain_conf_v2_GetGlobalVersionsRequest{ + limit = Limit, + continuation_token = ContinuationToken + } = Request, + %% Retrieve global versions with pagination + case dmt_repository:get_global_versions(Limit, ContinuationToken) of + {ok, Versions, NewToken} -> + {ok, #domain_conf_v2_GetVersionsResponse{ + result = Versions, + continuation_token = NewToken + }}; + {error, Reason} -> + woody_error:raise(system, {internal, Reason}) + end. diff --git a/apps/dmt/src/dmt_repository_handler.erl b/apps/dmt/src/dmt_repository_handler.erl new file mode 100644 index 0000000..d67e812 --- /dev/null +++ b/apps/dmt/src/dmt_repository_handler.erl @@ -0,0 +1,61 @@ +-module(dmt_repository_handler). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +%% API +-export([handle_function/4]). + +handle_function(Function, Args, WoodyContext0, Options) -> + DefaultDeadline = woody_deadline:from_timeout(default_handling_timeout(Options)), + WoodyContext = dmt_api_woody_utils:ensure_woody_deadline_set(WoodyContext0, DefaultDeadline), + do_handle_function(Function, Args, WoodyContext, Options). + +do_handle_function('Commit', {Version, Commit, CreatedBy}, _Context, _Options) -> + case dmt_repository:commit(Version, Commit, CreatedBy) of + {ok, NextVersion, NewObjects} -> + {ok, #domain_conf_v2_CommitResponse{ + version = NextVersion, + new_objects = ordsets:from_list(NewObjects) + }}; + {error, {operation_error, Error}} -> + woody_error:raise(business, handle_operation_error(Error)); + {error, version_not_found} -> + woody_error:raise(business, #domain_conf_v2_VersionNotFound{}); + {error, {head_mismatch, LatestVersion}} -> + woody_error:raise(business, #domain_conf_v2_ObsoleteCommitVersion{latest_version = LatestVersion}); + {error, migration_in_progress} -> + woody_error:raise(system, {internal, resource_unavailable, <<"Migration in progress. Please, stand by.">>}) + end. + +default_handling_timeout(#{default_handling_timeout := Timeout}) -> + Timeout. + +handle_operation_error({conflict, Conflict}) -> + #domain_conf_v2_OperationConflict{ + conflict = handle_operation_conflict(Conflict) + }; +handle_operation_error({invalid, Invalid}) -> + #domain_conf_v2_OperationInvalid{ + errors = handle_operation_invalid(Invalid) + }. + +handle_operation_conflict({object_already_exists, Ref}) -> + {object_already_exists, #domain_conf_v2_ObjectAlreadyExistsConflict{object_ref = Ref}}; +handle_operation_conflict({object_not_found, Ref}) -> + {object_not_found, #domain_conf_v2_ObjectNotFoundConflict{object_ref = Ref}}; +handle_operation_conflict({object_reference_mismatch, Ref}) -> + {object_reference_mismatch, #domain_conf_v2_ObjectReferenceMismatchConflict{object_ref = Ref}}. + +handle_operation_invalid({objects_not_exist, Refs}) -> + [ + {object_not_exists, #domain_conf_v2_NonexistantObject{ + object_ref = Ref, + referenced_by = ReferencedBy + }} + || {Ref, ReferencedBy} <- Refs + ]; +handle_operation_invalid({object_reference_cycles, Cycles}) -> + [ + {object_reference_cycle, #domain_conf_v2_ObjectReferenceCycle{cycle = Cycle}} + || Cycle <- Cycles + ]. diff --git a/apps/dmt/src/dmt_sup.erl b/apps/dmt/src/dmt_sup.erl new file mode 100644 index 0000000..fc96aa4 --- /dev/null +++ b/apps/dmt/src/dmt_sup.erl @@ -0,0 +1,144 @@ +%%%------------------------------------------------------------------- +%% @doc dmt top level supervisor. +%% @end +%%%------------------------------------------------------------------- + +-module(dmt_sup). + +-behaviour(supervisor). + +-export([start_link/0]). + +-export([init/1]). + +-export([get_service/1]). + +-define(SERVER, ?MODULE). +-define(APP, dmt). +-define(DEFAULT_DB, default_db). + +start_link() -> + supervisor:start_link({local, ?APP}, ?MODULE, []). + +%% sup_flags() = #{strategy => strategy(), % optional +%% intensity => non_neg_integer(), % optional +%% period => pos_integer()} % optional +%% child_spec() = #{id => child_id(), % mandatory +%% start => mfargs(), % mandatory +%% restart => restart(), % optional +%% shutdown => shutdown(), % optional +%% type => worker(), % optional +%% modules => modules()} % optional +init(_) -> + ok = dbinit(), + {ok, IP} = inet:parse_address(genlib_app:env(?APP, ip, "::")), + HealthCheck = enable_health_logging(genlib_app:env(?APP, health_check, #{})), + EventHandlers = genlib_app:env(?APP, woody_event_handlers, [scoper_woody_event_handler]), + API = woody_server:child_spec( + ?MODULE, + #{ + ip => IP, + port => genlib_app:env(?APP, port, 8022), + transport_opts => genlib_app:env(?APP, transport_opts, #{}), + protocol_opts => genlib_app:env(?APP, protocol_opts, #{}), + event_handler => EventHandlers, + handlers => get_repository_handlers(), + additional_routes => [ + get_prometheus_route(), + erl_health_handle:get_route(HealthCheck) + ] + } + ), + SupFlags = #{ + strategy => one_for_one, + intensity => 10, + period => 60 + }, + ChildSpecs = [API], + + {ok, {SupFlags, ChildSpecs}}. + +dbinit() -> + WorkDir = get_env_var("WORK_DIR"), + _ = set_database_url(), + MigrationsPath = WorkDir ++ "/migrations", + Cmd = "run", + case dmt_db_migration:process(["-d", MigrationsPath, Cmd]) of + ok -> ok; + {error, Reason} -> throw({migrations_error, Reason}) + end. + +set_database_url() -> + {ok, #{ + ?DEFAULT_DB := #{ + host := PgHost, + port := PgPort, + username := PgUser, + password := PgPassword, + database := DbName + } + }} = application:get_env(epg_connector, databases), + %% DATABASE_URL=postgresql://postgres:postgres@db/dmtv2 + PgPortStr = erlang:integer_to_list(PgPort), + Value = + "postgresql://" ++ PgUser ++ ":" ++ PgPassword ++ "@" ++ PgHost ++ ":" ++ PgPortStr ++ "/" ++ DbName, + true = os:putenv("DATABASE_URL", Value). + +%% internal functions + +get_env_var(Name) -> + case os:getenv(Name) of + false -> throw({os_env_required, Name}); + V -> V + end. + +get_repository_handlers() -> + DefaultTimeout = genlib_app:env(?APP, default_woody_handling_timeout, timer:seconds(30)), + [ + get_handler(repository, #{ + repository => dmt_repository_handler, + default_handling_timeout => DefaultTimeout + }), + get_handler(repository_client, #{ + repository => dmt_repository_client_handler, + default_handling_timeout => DefaultTimeout + }), + get_handler(user_op, #{ + repository => dmt_user_op_handler, + default_handling_timeout => DefaultTimeout + }) + ]. + +-spec get_handler(repository | repository_client | state_processor, woody:options()) -> + woody:http_handler(woody:th_handler()). +get_handler(repository, Options) -> + {"/v1/domain/repository", { + get_service(repository), + {dmt_repository_handler, Options} + }}; +get_handler(repository_client, Options) -> + {"/v1/domain/repository_client", { + get_service(repository_client), + {dmt_repository_client_handler, Options} + }}; +get_handler(user_op, Options) -> + {"/v1/domain/user_op", { + get_service(user_op), + {dmt_user_op_handler, Options} + }}. + +get_service(repository) -> + {dmsl_domain_conf_v2_thrift, 'Repository'}; +get_service(repository_client) -> + {dmsl_domain_conf_v2_thrift, 'RepositoryClient'}; +get_service(user_op) -> + {dmsl_domain_conf_v2_thrift, 'UserOpManagement'}. + +-spec enable_health_logging(erl_health:check()) -> erl_health:check(). +enable_health_logging(Check) -> + EvHandler = {erl_health_event_handler, []}, + maps:map(fun(_, V = {_, _, _}) -> #{runner => V, event_handler => EvHandler} end, Check). + +-spec get_prometheus_route() -> {iodata(), module(), _Opts :: any()}. +get_prometheus_route() -> + {"/metrics/[:registry]", prometheus_cowboy2_handler, []}. diff --git a/apps/dmt/src/dmt_user_op.erl b/apps/dmt/src/dmt_user_op.erl new file mode 100644 index 0000000..62895dd --- /dev/null +++ b/apps/dmt/src/dmt_user_op.erl @@ -0,0 +1,50 @@ +-module(dmt_user_op). + +%% Existing includes and exports +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). +-include_lib("epgsql/include/epgsql.hrl"). + +-define(POOL_NAME, user_op_pool). + +-export([ + insert_user/2, + get_user/1, + delete_user/1 +]). + +%% Insert a new user +insert_user(Name, Email) -> + Sql = "INSERT INTO op_user (name, email) VALUES ($1, $2) returning id", + Params = [Name, Email], + case epgsql_pool:query(?POOL_NAME, Sql, Params) of + {ok, 1, _Columns, [{ID}]} -> + {ok, ID}; + {error, Reason} -> + {error, Reason} + end. + +%% Retrieve a user by ID +get_user(UserOpID) -> + Sql = "SELECT id, name, email FROM op_user WHERE id = $1::uuid", + Params = [UserOpID], + case epgsql_pool:query(?POOL_NAME, Sql, Params) of + {ok, _Columns, [{ID, Name, Email}]} -> + {ok, #domain_conf_v2_UserOp{id = ID, name = Name, email = Email}}; + {ok, _, []} -> + {error, user_not_found}; + {error, Reason} -> + {error, Reason} + end. + +%% Delete a user by ID +delete_user(UserOpID) -> + Sql = "DELETE FROM op_user WHERE id = $1::uuid", + Params = [UserOpID], + case epgsql_pool:query(?POOL_NAME, Sql, Params) of + {ok, _, Result} when Result =:= [] -> + {error, user_not_found}; + {ok, 1} -> + ok; + {error, Reason} -> + {error, Reason} + end. diff --git a/apps/dmt/src/dmt_user_op_handler.erl b/apps/dmt/src/dmt_user_op_handler.erl new file mode 100644 index 0000000..a0deb41 --- /dev/null +++ b/apps/dmt/src/dmt_user_op_handler.erl @@ -0,0 +1,45 @@ +%% File: ./dmt/src/dmt_user_op_handler.erl + +-module(dmt_user_op_handler). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +%% API +-export([handle_function/4]). + +handle_function(Function, Args, WoodyContext0, Options) -> + DefaultDeadline = woody_deadline:from_timeout(default_handling_timeout(Options)), + WoodyContext = dmt_api_woody_utils:ensure_woody_deadline_set(WoodyContext0, DefaultDeadline), + do_handle_function(Function, Args, WoodyContext, Options). + +default_handling_timeout(#{default_handling_timeout := Timeout}) -> + Timeout. + +%% Implement the Create function +do_handle_function('Create', {Params}, _Context, _Options) -> + #domain_conf_v2_UserOpParams{email = Email, name = Name} = Params, + %% Insert into op_user table + case dmt_user_op:insert_user(Name, Email) of + {ok, ID} -> + {ok, #domain_conf_v2_UserOp{id = ID, email = Email, name = Name}}; + {error, Reason} -> + woody_error:raise(system, {internal, Reason}) + end; +do_handle_function('Get', {UserOpID}, _Context, _Options) -> + case dmt_user_op:get_user(UserOpID) of + {ok, #domain_conf_v2_UserOp{id = ID, email = Email, name = Name}} -> + {ok, #domain_conf_v2_UserOp{id = ID, email = Email, name = Name}}; + {error, user_not_found} -> + woody_error:raise(business, #domain_conf_v2_UserOpNotFound{}); + {error, Reason} -> + woody_error:raise(system, {internal, Reason}) + end; +do_handle_function('Delete', {UserOpID}, _Context, _Options) -> + case dmt_user_op:delete_user(UserOpID) of + ok -> + {ok, ok}; + {error, user_not_found} -> + woody_error:raise(business, #domain_conf_v2_UserOpNotFound{}); + {error, Reason} -> + woody_error:raise(system, {internal, Reason}) + end. diff --git a/apps/dmt/test/dmt_client.erl b/apps/dmt/test/dmt_client.erl new file mode 100644 index 0000000..f275a8d --- /dev/null +++ b/apps/dmt/test/dmt_client.erl @@ -0,0 +1,46 @@ +-module(dmt_client). + +-include_lib("opentelemetry_api/include/otel_tracer.hrl"). +-include_lib("opentelemetry_api/include/opentelemetry.hrl"). + +%% API +-export([ + checkout_object/3, + get_local_versions/2, + get_global_versions/2, + commit/4 +]). + +-export([ + create_user_op/2, + get_user_op/2, + delete_user_op/2 +]). + +checkout_object(VersionRef, ObjectRef, Client) -> + Args = [VersionRef, ObjectRef], + dmt_client_api:call(repository_client, 'CheckoutObject', Args, Client). + +get_local_versions(Request, Client) -> + Args = [Request], + dmt_client_api:call(repository_client, 'GetLocalVersions', Args, Client). + +get_global_versions(Request, Client) -> + Args = [Request], + dmt_client_api:call(repository_client, 'GetGlobalVersions', Args, Client). + +commit(Version, Commit, Author, Client) -> + Args = [Version, Commit, Author], + dmt_client_api:call(repository, 'Commit', Args, Client). + +create_user_op(Params, Client) -> + Args = [Params], + dmt_client_api:call(user_op, 'Create', Args, Client). + +get_user_op(ID, Client) -> + Args = [ID], + dmt_client_api:call(user_op, 'Get', Args, Client). + +delete_user_op(ID, Client) -> + Args = [ID], + dmt_client_api:call(user_op, 'Delete', Args, Client). diff --git a/apps/dmt/test/dmt_client_api.erl b/apps/dmt/test/dmt_client_api.erl new file mode 100644 index 0000000..be03413 --- /dev/null +++ b/apps/dmt/test/dmt_client_api.erl @@ -0,0 +1,38 @@ +-module(dmt_client_api). + +-export([new/1]). +-export([call/4]). + +-export_type([t/0]). + +%% + +-type t() :: woody_context:ctx(). + +-spec new(woody_context:ctx()) -> t(). +new(Context) -> + Context. + +-spec call(Name :: atom(), woody:func(), [any()], t()) -> {ok, _Response} | {exception, _} | {error, _}. +call(ServiceName, Function, Args, Context) -> + Service = dmt_sup:get_service(ServiceName), + Request = {Service, Function, list_to_tuple(Args)}, + Opts = get_opts(ServiceName), + try + woody_client:call(Request, Opts, Context) + catch + error:Error:ST -> + {error, {Error, ST}} + end. + +get_opts(ServiceName) -> + EventHandlerOpts = genlib_app:env(dmt, scoper_event_handler_options, #{}), + Opts0 = #{ + event_handler => {scoper_woody_event_handler, EventHandlerOpts} + }, + case maps:get(ServiceName, genlib_app:env(dmt, services), undefined) of + #{} = Opts -> + maps:merge(Opts, Opts0); + _ -> + Opts0 + end. diff --git a/apps/dmt/test/dmt_ct_helper.erl b/apps/dmt/test/dmt_ct_helper.erl new file mode 100644 index 0000000..4a4662e --- /dev/null +++ b/apps/dmt/test/dmt_ct_helper.erl @@ -0,0 +1,169 @@ +-module(dmt_ct_helper). + +-export([start_app/1]). +-export([start_app/2]). +-export([start_apps/1]). + +-export([cfg/2]). + +-export([create_client/0]). +-export([create_client/1]). + +-export([cleanup_db/0]). + +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-export_type([config/0]). +-export_type([test_case_name/0]). +-export_type([group_name/0]). + +%% + +-type app_name() :: atom(). + +-spec start_app(app_name()) -> {[app_name()], map()}. +start_app(scoper = AppName) -> + { + start_app(AppName, [ + {storage, scoper_storage_logger} + ]), + #{} + }; +start_app(woody = AppName) -> + { + start_app(AppName, [ + {acceptors_pool_size, 4} + ]), + #{} + }; +start_app(dmt = AppName) -> + { + start_app(AppName, [ + {host, <<"dominant-v2">>}, + {port, 8022}, + {scoper_event_handler_options, #{ + event_handler_opts => #{ + formatter_opts => #{ + max_length => 1000 + } + } + }}, + {services, #{ + repository => #{ + url => <<"http://dominant-v2:8022/v1/domain/repository">> + }, + repository_client => #{ + url => <<"http://dominant-v2:8022/v1/domain/repository_client">> + }, + user_op => #{ + url => <<"http://dominant-v2:8022/v1/domain/user_op">> + } + }} + ]), + #{} + }; +start_app(epg_connector = AppName) -> + { + start_app(AppName, [ + {databases, #{ + default_db => #{ + host => "db", + port => 5432, + username => "postgres", + password => "postgres", + database => "dmt" + } + }}, + {pools, #{ + default_pool => #{ + database => default_db, + size => 10 + }, + user_op_pool => #{ + database => default_db, + size => 10 + } + }} + ]), + #{} + }; +start_app(AppName) -> + {genlib_app:start_application(AppName), #{}}. + +-spec start_app(app_name(), list()) -> [app_name()]. +start_app(cowboy = AppName, Env) -> + #{ + listener_ref := Ref, + acceptors_count := Count, + transport_opts := TransOpt, + proto_opts := ProtoOpt + } = Env, + {ok, _} = cowboy:start_clear(Ref, [{num_acceptors, Count} | TransOpt], ProtoOpt), + [AppName]; +start_app(AppName, Env) -> + genlib_app:start_application_with(AppName, Env). + +-spec start_apps([app_name() | {app_name(), list()}]) -> {[app_name()], map()}. +start_apps(Apps) -> + lists:foldl( + fun + ({AppName, Env}, {AppsAcc, RetAcc}) -> + {lists:reverse(start_app(AppName, Env)) ++ AppsAcc, RetAcc}; + (AppName, {AppsAcc, RetAcc}) -> + {Apps0, Ret0} = start_app(AppName), + {lists:reverse(Apps0) ++ AppsAcc, maps:merge(Ret0, RetAcc)} + end, + {[], #{}}, + Apps + ). + +-type config() :: [{atom(), term()}]. +-type test_case_name() :: atom(). +-type group_name() :: atom(). + +-spec cfg(atom(), config()) -> term(). +cfg(Key, Config) -> + case lists:keyfind(Key, 1, Config) of + {Key, V} -> V; + _ -> undefined + end. + +%% + +-define(ROOT_URL, "http://dominant-v2:8022"). + +-spec create_client() -> dmt_client_api:t(). +create_client() -> + create_client_w_context(woody_context:new()). +%% {?ROOT_URL, create_client_w_context(woody_context:new())}. + +-spec create_client(woody:trace_id()) -> dmt_client_api:t(). +create_client(TraceID) -> + create_client_w_context(woody_context:new(TraceID)). +%% {?ROOT_URL, create_client_w_context(woody_context:new(TraceID))}. + +create_client_w_context(WoodyCtx) -> + dmt_client_api:new(WoodyCtx). + +-spec cleanup_db() -> ok. +cleanup_db() -> + Query = """ + DO $$ + DECLARE + r RECORD; + BEGIN + -- Loop through all tables in the current schema + FOR r IN ( + SELECT table_name + FROM information_schema.tables + WHERE table_schema='public' + AND NOT table_name = '__migrations' + ) LOOP + -- Execute the TRUNCATE command on each table + EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.table_name) || ' RESTART IDENTITY CASCADE'; + END LOOP; + END $$; + """, + {ok, _, _} = epgsql_pool:query(default_pool, Query), + ok. diff --git a/apps/dmt/test/dmt_integration_test_SUITE.erl b/apps/dmt/test/dmt_integration_test_SUITE.erl new file mode 100644 index 0000000..d08b5d7 --- /dev/null +++ b/apps/dmt/test/dmt_integration_test_SUITE.erl @@ -0,0 +1,371 @@ +-module(dmt_integration_test_SUITE). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +%% API +-export([ + init_per_suite/1, + end_per_suite/1, + init_per_group/2, + end_per_group/2, + end_per_testcase/2, + all/0, + groups/0 +]). + +-export([ + %% UserOpManagement Tests + create_user_op_test/1, + get_user_op_test/1, + delete_user_op_test/1 +]). + +-export([ + %% Repository Tests + insert_object_forced_id_success_test/1, + insert_object_sequence_id_success_test/1, + insert_remove_referencing_object_success_test/1, + update_object_success_test/1 +]). + +-export([ + %% RepositoryClient Tests +]). + +%% Setup and Teardown + +%% Initialize per suite +init_per_suite(Config) -> + {Apps, _Ret} = dmt_ct_helper:start_apps([woody, scoper, epg_connector, dmt]), + ApiClient = dmt_ct_helper:create_client(), + [{client, ApiClient}, {apps, Apps} | Config]. + +%% Cleanup after suite +end_per_suite(_Config) -> + dmt_ct_helper:cleanup_db(), + ok. + +%% Define all test cases +all() -> + [ + {group, create_user_op_test}, + {group, repository_tests} +%% {group, repository_client_tests} + ]. + +%% Define test groups +groups() -> + [ + {create_user_op_test, [parallel], [ + create_user_op_test, + get_user_op_test, + delete_user_op_test + ]}, + {repository_tests, [], [ + insert_object_forced_id_success_test, + insert_object_sequence_id_success_test, + insert_remove_referencing_object_success_test, + update_object_success_test + ]}, + {repository_client_tests, [], [ + + ]} + ]. + +init_per_group(repository_client_tests, C) -> + Client = dmt_ct_helper:cfg(client, C), + [ + {user_op_id, create_user_op(<<"repository_client_tests@tests">>, Client)} + | C + ]; +init_per_group(_, C) -> + C. + +end_per_group(_, _C) -> + ok. + +end_per_testcase(_, _C) -> + dmt_ct_helper:cleanup_db(), + ok. + +%% Test Cases + +%% UserOpManagement Tests + +create_user_op_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + UserOpParams = #domain_conf_v2_UserOpParams{ + email = <<"create_user_op_test@test">>, + name = <<"some_name">> + }, + + {ok, _} = dmt_client:create_user_op(UserOpParams, Client). + +get_user_op_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + Email = <<"get_user_op_test">>, + + UserOpID = create_user_op(Email, Client), + + {ok, #domain_conf_v2_UserOp{ + email = Email1 + }} = dmt_client:get_user_op(UserOpID, Client), + ?assertEqual(Email, Email1). + +delete_user_op_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + Email = <<"delete_user_op_test">>, + + UserOpID = create_user_op(Email, Client), + + {ok, ok} = dmt_client:delete_user_op(UserOpID, Client), + + {exception, #domain_conf_v2_UserOpNotFound{}} = + dmt_client:get_user_op(UserOpID, Client). + +%% Repository tests + +insert_remove_referencing_object_success_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + Email = <<"insert_remove_referencing_object_success_test">>, + UserOpID = create_user_op(Email, Client), + + Revision1 = 0, + Commit1 = #domain_conf_v2_Commit{ + ops = [ + {insert, #domain_conf_v2_InsertOp{ + object = {proxy, #domain_ProxyDefinition{ + name = <<"proxy">>, + description = <<"proxy_description">>, + url = <<"http://someurl">>, + options = #{} + }} + }} + ] + }, + {ok, #domain_conf_v2_CommitResponse{ + version = Revision2, + new_objects = [ + {proxy, #domain_ProxyObject{ + ref = ProxyRef + }} + ] + }} = dmt_client:commit(Revision1, Commit1, UserOpID, Client), + + + Commit2 = #domain_conf_v2_Commit{ + ops = [ + {insert, #domain_conf_v2_InsertOp{ + object = {provider, #domain_Provider{ + name = <<"name">>, + description = <<"description">>, + proxy = #domain_Proxy{ + ref = ProxyRef, + additional = #{} + } + }} + }} + ] + }, + + {ok, #domain_conf_v2_CommitResponse{ + version = Revision3, + new_objects = [ + {provider, #domain_ProviderObject{ + ref = _ProviderRef + }} + ] + }} = dmt_client:commit(Revision2, Commit2, UserOpID, Client), + +%% try to remove proxy + Commit3 = #domain_conf_v2_Commit{ + ops = [ + {remove, #domain_conf_v2_RemoveOp{ + ref = {proxy, ProxyRef} + }} + ] + }, + + + {ok, _} = dmt_client:commit(Revision3, Commit3, UserOpID, Client). + +%% FIXME reference collecting doesn't work. Need to fix ASAP + +%% +%%%% try to remove provider +%% Commit4 = #domain_conf_v2_Commit{ +%% ops = [ +%% {remove, #domain_conf_v2_RemoveOp{ +%% ref = {provider, ProviderRef} +%% }} +%% ] +%% }, +%% {ok, #domain_conf_v2_CommitResponse{ +%% version = Revision4 +%% }} = dmt_client:commit(Revision3, Commit4, UserOpID, Client), +%% +%%%% try to remove proxy again +%% {ok, #domain_conf_v2_CommitResponse{}} = +%% dmt_client:commit(Revision4, Commit3, UserOpID, Client). + +insert_object_sequence_id_success_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + Email = <<"insert_object_sequence_id_success_test">>, + UserOpID = create_user_op(Email, Client), + + %% Insert a test object + Revision = 0, + Category = #domain_Category{ + name = <<"name1">>, + description = <<"description1">> + }, + Commit = #domain_conf_v2_Commit{ + ops = [ + {insert, #domain_conf_v2_InsertOp{ + object = {category, Category} + }}, + {insert, #domain_conf_v2_InsertOp{ + object = {category, Category} + }} + ] + }, + + {ok, #domain_conf_v2_CommitResponse{ + new_objects = [ + {category, #domain_CategoryObject{ + ref = #domain_CategoryRef{id = ID1} + }}, + {category, #domain_CategoryObject{ + ref = #domain_CategoryRef{id = ID2} + }} + ] + }} = dmt_client:commit(Revision, Commit, UserOpID, Client), + + ?assertMatch(true, is_in_sequence(ID1, ID2)). + +is_in_sequence(N1, N2) when N1 + 1 =:= N2 -> + true; +is_in_sequence(N1, N2) when N1 =:= N2 + 1 -> + true; +is_in_sequence(N1, N2) -> + {false, N1, N2}. + +%% Test successful insert with forced id +insert_object_forced_id_success_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + Email = <<"insert_object_forced_id_success_test">>, + UserOpID = create_user_op(Email, Client), + + %% Insert a test object + Revision = 0, + CategoryRef = #domain_CategoryRef{id = 1337}, + ForcedRef = {category, CategoryRef}, + Category = #domain_Category{ + name = <<"name">>, + description = <<"description">> + }, + Commit = #domain_conf_v2_Commit{ + ops = [ + {insert, #domain_conf_v2_InsertOp{ + object = + {category, Category}, + force_ref = ForcedRef + }} + ] + }, + + {ok, #domain_conf_v2_CommitResponse{ + new_objects = NewObjectsSet + }} = dmt_client:commit(Revision, Commit, UserOpID, Client), + + [ + {category, #domain_CategoryObject{ + ref = Ref, + data = Category + }} + ] = ordsets:to_list(NewObjectsSet), + ?assertMatch(CategoryRef, Ref). + + +update_object_success_test(Config) -> + Client = dmt_ct_helper:cfg(client, Config), + + Email = <<"insert_remove_referencing_object_success_test">>, + UserOpID = create_user_op(Email, Client), + + Revision1 = 0, + Commit1 = #domain_conf_v2_Commit{ + ops = [ + {insert, #domain_conf_v2_InsertOp{ + object = {proxy, #domain_ProxyDefinition{ + name = <<"proxy">>, + description = <<"proxy_description">>, + url = <<"http://someurl">>, + options = #{} + }} + }} + ] + }, + {ok, #domain_conf_v2_CommitResponse{ + version = Revision2, + new_objects = [ + {proxy, #domain_ProxyObject{ + ref = ProxyRef + }} + ] + }} = dmt_client:commit(Revision1, Commit1, UserOpID, Client), + + + NewObject = {proxy, #domain_ProxyObject{ + ref = ProxyRef, + data = #domain_ProxyDefinition{ + name = <<"proxy2">>, + description = <<"proxy_description2">>, + url = <<"http://someurl">>, + options = #{} + } + }}, + + Commit2 = #domain_conf_v2_Commit{ + ops = [ + {update, #domain_conf_v2_UpdateOp{ + targeted_ref = {proxy, ProxyRef}, + new_object = NewObject + }} + ] + }, + + {ok, #domain_conf_v2_CommitResponse{ + version = Revision3 + }} = dmt_client:commit(Revision2, Commit2, UserOpID, Client), + + {ok, #domain_conf_v2_VersionedObject{ + object = NewObject + }} = dmt_client:checkout_object({version, Revision3}, {proxy, ProxyRef}, Client). + +%% RepositoryClient Tests + +%% GetLocalVersions + +%% GetGlobalVersions + +create_user_op(Email, Client) -> + %% Create UserOp + UserOpParams = #domain_conf_v2_UserOpParams{ + email = Email, + name = <<"some_name">> + }, + + {ok, #domain_conf_v2_UserOp{ + id = UserOpID + }} = dmt_client:create_user_op(UserOpParams, Client), + UserOpID. diff --git a/apps/dmt_core/src/dmt_core.app.src b/apps/dmt_core/src/dmt_core.app.src new file mode 100644 index 0000000..6e6bdbc --- /dev/null +++ b/apps/dmt_core/src/dmt_core.app.src @@ -0,0 +1,14 @@ +{application, dmt_core, [ + {description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {env, []}, + {modules, []}, + + {licenses, ["Apache-2.0"]}, + {links, []} +]}. diff --git a/apps/dmt_core/src/dmt_domain.erl b/apps/dmt_core/src/dmt_domain.erl new file mode 100644 index 0000000..45f29e1 --- /dev/null +++ b/apps/dmt_core/src/dmt_domain.erl @@ -0,0 +1,155 @@ +-module(dmt_domain). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +-compile({parse_transform, dmt_domain_pt}). + +%% + +-export([references/1]). + +-define(DOMAIN, dmsl_domain_thrift). + +-export_type([operation_error/0]). + +%% + +-type object_ref() :: dmsl_domain_thrift:'Reference'(). +-type domain_object() :: dmsl_domain_thrift:'DomainObject'(). + +-type nonexistent_object() :: {object_ref(), [object_ref()]}. +-type operation_conflict() :: + {object_already_exists, object_ref()} + | {object_not_found, object_ref()} + | {object_reference_mismatch, object_ref()}. + +-type operation_invalid() :: + {objects_not_exist, [nonexistent_object()]} + | {object_reference_cycles, [[object_ref()]]}. + +-type operation_error() :: + {conflict, operation_conflict()} + | {invalid, operation_invalid()}. + +references(DomainObject) -> + {DataType, Data} = get_data(DomainObject), + references(Data, DataType). + +references(Object, DataType) -> + references(Object, DataType, []). + +references(undefined, _StructInfo, Refs) -> + Refs; +references({Tag, Object}, StructInfo = {struct, union, FieldsInfo}, Refs) when is_list(FieldsInfo) -> + case get_field_info(Tag, StructInfo) of + false -> + erlang:error({<<"field info not found">>, Tag, StructInfo}); + {_, _, Type, _, _} -> + check_reference_type(Object, Type, Refs) + end; +%% what if it's a union? +references(Object, {struct, struct, FieldsInfo}, Refs) when is_list(FieldsInfo) -> + indexfold( + fun(I, {_, _Required, FieldType, _Name, _}, Acc) -> + case element(I, Object) of + undefined -> + Acc; + Field -> + check_reference_type(Field, FieldType, Acc) + end + end, + Refs, + % NOTE + % This `2` gives index of the first significant field in a record tuple. + 2, + FieldsInfo + ); +references(Object, {struct, _, {?DOMAIN, StructName}}, Refs) -> + StructInfo = get_struct_info(StructName), + check_reference_type(Object, StructInfo, Refs); +references(Object, {list, FieldType}, Refs) -> + lists:foldl( + fun(O, Acc) -> + check_reference_type(O, FieldType, Acc) + end, + Refs, + Object + ); +references(Object, {set, FieldType}, Refs) -> + ListObject = ordsets:to_list(Object), + check_reference_type(ListObject, {list, FieldType}, Refs); +references(Object, {map, KeyType, ValueType}, Refs) -> + maps:fold( + fun(K, V, Acc) -> + check_reference_type( + V, + ValueType, + check_reference_type(K, KeyType, Acc) + ) + end, + Refs, + Object + ); +references(_DomainObject, _Primitive, Refs) -> + Refs. + +indexfold(Fun, Acc, I, [E | Rest]) -> + indexfold(Fun, Fun(I, E, Acc), I + 1, Rest); +indexfold(_Fun, Acc, _I, []) -> + Acc. + +check_reference_type(Object, Type, Refs) -> + case is_reference_type(Type) of + {true, Tag} -> + [{Tag, Object} | Refs]; + false -> + references(Object, Type, Refs) + end. + +-spec get_data(domain_object()) -> any(). +get_data(DomainObject) -> + get_domain_object_field(data, DomainObject). + +get_domain_object_field(Field, {Tag, Struct}) -> + get_field(Field, Struct, get_domain_object_schema(Tag)). + +get_domain_object_schema(Tag) -> + SchemaInfo = get_struct_info('DomainObject'), + {_, _, {struct, _, {_, ObjectStructName}}, _, _} = get_field_info(Tag, SchemaInfo), + get_struct_info(ObjectStructName). + +get_field(Field, Struct, StructInfo) when is_atom(Field) -> + {FieldIndex, {_, _, Type, _, _}} = get_field_index(Field, StructInfo), + {Type, element(FieldIndex, Struct)}. + +get_struct_info(StructName) -> + dmsl_domain_thrift:struct_info(StructName). + +get_field_info(Field, {struct, _StructType, FieldsInfo}) -> + lists:keyfind(Field, 4, FieldsInfo). + +get_field_index(Field, {struct, _StructType, FieldsInfo}) -> + % NOTE + % This `2` gives index of the first significant field in a record tuple. + get_field_index(Field, 2, FieldsInfo). + +get_field_index(_Field, _, []) -> + false; +get_field_index(Field, I, [F | Rest]) -> + case F of + {_, _, _, Field, _} = Info -> + {I, Info}; + _ -> + get_field_index(Field, I + 1, Rest) + end. + +is_reference_type(Type) -> + {struct, union, StructInfo} = get_struct_info('Reference'), + is_reference_type(Type, StructInfo). + +is_reference_type(_Type, []) -> + false; +is_reference_type(Type, [{_, _, Type, Tag, _} | _Rest]) -> + {true, Tag}; +is_reference_type(Type, [_ | Rest]) -> + is_reference_type(Type, Rest). diff --git a/apps/dmt_core/src/dmt_domain_pt.erl b/apps/dmt_core/src/dmt_domain_pt.erl new file mode 100644 index 0000000..81c67bc --- /dev/null +++ b/apps/dmt_core/src/dmt_domain_pt.erl @@ -0,0 +1,92 @@ +-module(dmt_domain_pt). + +-export([parse_transform/2]). + +-spec parse_transform(Forms, [compile:option()]) -> Forms when + Forms :: [erl_parse:abstract_form() | erl_parse:form_info()]. +parse_transform(Forms, _Options) -> + [ + erl_syntax:revert(FormNext) + || Form <- Forms, + FormNext <- [erl_syntax_lib:map(fun transform/1, Form)], + FormNext /= delete + ]. + +transform(Form) -> + case erl_syntax:type(Form) of + function -> + Name = erl_syntax:concrete(erl_syntax:function_name(Form)), + Arity = erl_syntax:function_arity(Form), + transform_function(Name, Arity, Form); + _ -> + Form + end. + +transform_function(Name = is_reference_type, 1, FormWas) -> + % NOTE + % Replacing `dmt_domain:is_reference_type/1` with a code which does something similar to: + % ``` + % is_reference_type({struct, struct, {dmsl_domain_thrift, 'CategoryRef'}}) -> {true, 'category'}; + % is_reference_type({struct, struct, {dmsl_domain_thrift, 'CurrencyRef'}}) -> {true, 'currency'}; + % ... + % is_reference_type(_) -> false. + % ``` + {struct, union, StructInfo} = get_struct_info('Reference'), + ok = validate_reference_struct('Reference', StructInfo), + Clauses = + [ + erl_syntax:clause( + [erl_syntax:abstract(Type)], + none, + [erl_syntax:abstract({true, Tag})] + ) + || {_N, _Req, Type, Tag, _Default} <- StructInfo + ] ++ + [ + erl_syntax:clause( + [erl_syntax:underscore()], + none, + [erl_syntax:abstract(false)] + ) + ], + Form = erl_syntax_lib:map( + fun(F) -> erl_syntax:copy_attrs(FormWas, F) end, + erl_syntax:function( + erl_syntax:abstract(Name), + Clauses + ) + ), + Form; +transform_function(_Name = is_reference_type, 2, _FormWas) -> + % NOTE + % We need to make `is_reference_type/2` disappear, otherwise it will trigger _unused function_ + % warning. + delete; +transform_function(_, _, Form) -> + Form. + +get_struct_info(StructName) -> + dmsl_domain_thrift:struct_info(StructName). + +validate_reference_struct(StructName, StructInfo) -> + Mappings = lists:foldl( + fun({N, _Req, Type, Tag, _Default}, Acc) -> + maps:put(Type, [{N, Tag} | maps:get(Type, Acc, [])], Acc) + end, + #{}, + StructInfo + ), + case maps:filter(fun(_, Tags) -> length(Tags) > 1 end, Mappings) of + Dups when map_size(Dups) > 0 -> + ErrorMessage = format( + "struct_info(~0p): multiple fields share the same reference type, " + "this breaks referential integrity validation", + [StructName] + ), + exit({ErrorMessage, Dups}); + _ -> + ok + end. + +format(Fmt, Args) -> + unicode:characters_to_nfc_list(io_lib:format(Fmt, Args)). diff --git a/apps/dmt_core/src/dmt_history.erl b/apps/dmt_core/src/dmt_history.erl new file mode 100644 index 0000000..2707484 --- /dev/null +++ b/apps/dmt_core/src/dmt_history.erl @@ -0,0 +1,50 @@ +-module(dmt_history). + +-export([head/1]). +-export([head/2]). +-export([travel/3]). + +-include_lib("damsel/include/dmsl_domain_conf_thrift.hrl"). + +-type history() :: dmsl_domain_conf_thrift:'History'(). +-type version() :: dmsl_domain_conf_thrift:'Version'(). +-type snapshot() :: dmsl_domain_conf_thrift:'Snapshot'(). + +-spec head(history()) -> {ok, snapshot()} | {error, dmt_domain:operation_error()}. +head(History) -> + head(History, #domain_conf_Snapshot{version = 0, domain = dmt_domain:new()}). + +-spec head(history(), snapshot()) -> {ok, snapshot()} | {error, dmt_domain:operation_error()}. +head(History, Snapshot) when map_size(History) =:= 0 -> + {ok, Snapshot}; +head(History, Snapshot) -> + Head = lists:max(maps:keys(History)), + travel(Head, History, Snapshot). + +-spec travel(version(), history(), snapshot()) -> {ok, snapshot()} | {error, dmt_domain:operation_error()}. +travel(To, _History, #domain_conf_Snapshot{version = From} = Snapshot) when To =:= From -> + {ok, Snapshot}; +travel(To, History, #domain_conf_Snapshot{version = From, domain = Domain}) when To > From -> + #domain_conf_Commit{ops = Ops} = maps:get(From + 1, History), + case dmt_domain:apply_operations(Ops, Domain) of + {ok, NewDomain} -> + NextSnapshot = #domain_conf_Snapshot{ + version = From + 1, + domain = NewDomain + }, + travel(To, History, NextSnapshot); + {error, _} = Error -> + Error + end; +travel(To, History, #domain_conf_Snapshot{version = From, domain = Domain}) when To < From -> + #domain_conf_Commit{ops = Ops} = maps:get(From, History), + case dmt_domain:revert_operations(Ops, Domain) of + {ok, NewDomain} -> + PreviousSnapshot = #domain_conf_Snapshot{ + version = From - 1, + domain = NewDomain + }, + travel(To, History, PreviousSnapshot); + {error, _} = Error -> + Error + end. diff --git a/apps/dmt_object/src/dmt_object.app.src b/apps/dmt_object/src/dmt_object.app.src new file mode 100644 index 0000000..afac4b9 --- /dev/null +++ b/apps/dmt_object/src/dmt_object.app.src @@ -0,0 +1,14 @@ +{application, dmt_object, [ + {description, "An OTP application"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {env, []}, + {modules, []}, + + {licenses, ["Apache-2.0"]}, + {links, []} +]}. diff --git a/apps/dmt_object/src/dmt_object.erl b/apps/dmt_object/src/dmt_object.erl new file mode 100644 index 0000000..ddbc4a9 --- /dev/null +++ b/apps/dmt_object/src/dmt_object.erl @@ -0,0 +1,125 @@ +-module(dmt_object). + +-feature(maybe_expr, enable). + +-include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). + +-export([new_object/1]). +-export([update_object/2]). +-export([remove_object/1]). +-export([just_object/7]). + +-export_type([insertable_object/0]). +-export_type([object_changes/0]). +-export_type([object/0]). + +-type object_type() :: atom(). +-type object_id() :: string(). +-type timestamp() :: string(). + +-type insertable_object() :: #{ + type := object_type(), + tmp_id := object_id(), + forced_id := string() | undefined, + references := [{object_type(), object_id()}], + data := binary() +}. + +-type object_changes() :: #{ + id := object_id(), + type := object_type(), + references => [{object_type(), object_id()}], + referenced_by => [{object_type(), object_id()}], + data => binary(), + is_active => boolean() +}. + +-type object() :: #{ + id := object_id(), + type := object_type(), + global_version := string(), + references := [{object_type(), object_id()}], + referenced_by := [{object_type(), object_id()}], + data := binary(), + created_at := timestamp(), + created_by := string() +}. + +new_object(#domain_conf_v2_InsertOp{ + object = NewObject, + force_ref = ForcedRef +}) -> + case get_checked_type(ForcedRef, NewObject) of + {ok, Type} -> + {ok, #{ + tmp_id => uuid:get_v4_urandom(), + type => Type, + forced_id => ForcedRef, + references => list_term_to_binary(dmt_object_reference:refless_object_references(NewObject)), + data => NewObject + }}; + {error, Error} -> + {error, Error} + end. + +update_object( + #domain_conf_v2_UpdateOp{ + targeted_ref = {_, ID} = TargetedRef, + new_object = NewObject + }, + ExistingUpdate +) -> + maybe + {ok, Type} ?= get_checked_type(TargetedRef, NewObject), + ok ?= check_domain_object_refs(TargetedRef, NewObject), + {ok, ExistingUpdate#{ + id => ID, + type => Type, + %% NOTE this will just provide all the refs that already exist, + %% it doesn't give us diff, but maybe it's not needed + references => dmt_object_reference:domain_object_references(NewObject), + data => NewObject + }} + end. + +remove_object(OG) -> + OG#{ + referenced_by => [], + is_active => false + }. + +just_object( + ID, + Version, + ReferencesTo, + ReferencedBy, + Data, + CreatedAt, + IsActive +) -> + {Type, _} = ID, + #{ + id => ID, + type => Type, + global_version => Version, + references => ReferencesTo, + referenced_by => ReferencedBy, + data => Data, + created_at => CreatedAt, + is_active => IsActive + }. + +get_checked_type(undefined, {Type, _}) -> + {ok, Type}; +get_checked_type({Type, _}, {Type, _}) -> + {ok, Type}; +get_checked_type(Ref, Object) -> + {error, {type_mismatch, Ref, Object}}. + +check_domain_object_refs({Type, Ref}, {Type, {_Object, Ref, _Data}}) -> + ok; +check_domain_object_refs(Ref, Object) -> + {error, {reference_mismatch, Ref, Object}}. + +list_term_to_binary(Terms) -> + lists:map(fun(Term) -> term_to_binary(Term) end, Terms). diff --git a/apps/dmt_object/src/dmt_object_id.erl b/apps/dmt_object/src/dmt_object_id.erl new file mode 100644 index 0000000..fc1b554 --- /dev/null +++ b/apps/dmt_object/src/dmt_object_id.erl @@ -0,0 +1,45 @@ +-module(dmt_object_id). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% API +-export([get_numerical_object_id/2]). + +get_numerical_object_id(category, ID) -> + #domain_CategoryRef{id = ID}; +get_numerical_object_id(business_schedule, ID) -> + #domain_BusinessScheduleRef{id = ID}; +get_numerical_object_id(calendar, ID) -> + #domain_CalendarRef{id = ID}; +get_numerical_object_id(bank, ID) -> + #domain_BankRef{id = ID}; +get_numerical_object_id(contract_template, ID) -> + #domain_ContractTemplateRef{id = ID}; +get_numerical_object_id(term_set_hierarchy, ID) -> + #domain_TermSetHierarchyRef{id = ID}; +get_numerical_object_id(payment_institution, ID) -> + #domain_PaymentInstitutionRef{id = ID}; +get_numerical_object_id(provider, ID) -> + #domain_ProviderRef{id = ID}; +get_numerical_object_id(terminal, ID) -> + #domain_TerminalRef{id = ID}; +get_numerical_object_id(inspector, ID) -> + #domain_InspectorRef{id = ID}; +get_numerical_object_id(system_account_set, ID) -> + #domain_SystemAccountSetRef{id = ID}; +get_numerical_object_id(external_account_set, ID) -> + #domain_ExternalAccountSetRef{id = ID}; +get_numerical_object_id(proxy, ID) -> + #domain_ProxyRef{id = ID}; +get_numerical_object_id(cash_register_provider, ID) -> + #domain_CashRegisterProviderRef{id = ID}; +get_numerical_object_id(routing_rules, ID) -> + #domain_RoutingRulesetRef{id = ID}; +get_numerical_object_id(bank_card_category, ID) -> + #domain_BankCardCategoryRef{id = ID}; +get_numerical_object_id(criterion, ID) -> + #domain_CriterionRef{id = ID}; +get_numerical_object_id(document_type, ID) -> + #domain_DocumentTypeRef{id = ID}; +get_numerical_object_id(Type, _ID) -> + throw({not_supported, Type}). diff --git a/apps/dmt_object/src/dmt_object_reference.erl b/apps/dmt_object/src/dmt_object_reference.erl new file mode 100644 index 0000000..8a47e76 --- /dev/null +++ b/apps/dmt_object/src/dmt_object_reference.erl @@ -0,0 +1,156 @@ +-module(dmt_object_reference). + +%% API + +-export([get_domain_object_ref/1]). +-export([refless_object_references/1]). +-export([domain_object_references/1]). + +-define(DOMAIN, dmsl_domain_thrift). + +get_domain_object_ref(DomainObject = {Tag, _Struct}) -> + {_Type, Ref} = get_domain_object_field(ref, DomainObject), + {Tag, Ref}. + +%% RefflessObject ZONE + +%% FIXME doesn't work +refless_object_references(DomainObject) -> + {Data, DataType} = get_refless_data(DomainObject), + references(Data, DataType). + +get_refless_data({Tag, Struct}) -> + {Struct, get_refless_object_schema(Tag)}. + +get_refless_object_schema(Tag) -> + SchemaInfo = get_struct_info('ReflessDomainObject'), + {_, _, {struct, _, {_, ObjectStructName}}, _, _} = get_field_info(Tag, SchemaInfo), + {ObjectStructName, get_struct_info(ObjectStructName)}. + +%% DomainObject ZONE + +domain_object_references(DomainObject) -> + {Data, DataType} = get_domain_object_data(DomainObject), + references(Data, DataType). + +get_domain_object_data(DomainObject) -> + get_domain_object_field(data, DomainObject). + +get_domain_object_field(Field, {Tag, Struct}) -> + get_field(Field, Struct, get_domain_object_schema(Tag)). + +get_domain_object_schema(Tag) -> + SchemaInfo = get_struct_info('DomainObject'), + {_, _, {struct, _, {_, ObjectStructName}}, _, _} = get_field_info(Tag, SchemaInfo), + get_struct_info(ObjectStructName). + +get_field(Field, Struct, StructInfo) when is_atom(Field) -> + {FieldIndex, {_, _, Type, _, _}} = get_field_index(Field, StructInfo), + {element(FieldIndex, Struct), Type}. + +get_field_index(Field, {struct, _StructType, FieldsInfo}) -> + % NOTE + % This `2` gives index of the first significant field in a record tuple. + get_field_index(Field, 2, FieldsInfo). + +get_field_index(_Field, _, []) -> + false; +get_field_index(Field, I, [F | Rest]) -> + case F of + {_, _, _, Field, _} = Info -> + {I, Info}; + _ -> + get_field_index(Field, I + 1, Rest) + end. + +%% References Gathering ZONE + +references(Object, DataType) -> + references(Object, DataType, []). + +references(undefined, _StructInfo, Refs) -> + Refs; +references({Tag, Object}, StructInfo = {struct, union, FieldsInfo}, Refs) when is_list(FieldsInfo) -> + case get_field_info(Tag, StructInfo) of + false -> + erlang:error({<<"field info not found">>, Tag, StructInfo}); + {_, _, Type, _, _} -> + check_reference_type(Object, Type, Refs) + end; +%% what if it's a union? +references(Object, {struct, struct, FieldsInfo}, Refs) when is_list(FieldsInfo) -> + indexfold( + fun(I, {_, _Required, FieldType, _Name, _}, Acc) -> + case element(I, Object) of + undefined -> + Acc; + Field -> + check_reference_type(Field, FieldType, Acc) + end + end, + Refs, + % NOTE + % This `2` gives index of the first significant field in a record tuple. + 2, + FieldsInfo + ); +references(Object, {struct, _, {?DOMAIN, StructName}}, Refs) -> + StructInfo = get_struct_info(StructName), + check_reference_type(Object, StructInfo, Refs); +references(Object, {list, FieldType}, Refs) -> + lists:foldl( + fun(O, Acc) -> + check_reference_type(O, FieldType, Acc) + end, + Refs, + Object + ); +references(Object, {set, FieldType}, Refs) -> + ListObject = ordsets:to_list(Object), + check_reference_type(ListObject, {list, FieldType}, Refs); +references(Object, {map, KeyType, ValueType}, Refs) -> + maps:fold( + fun(K, V, Acc) -> + check_reference_type( + V, + ValueType, + check_reference_type(K, KeyType, Acc) + ) + end, + Refs, + Object + ); +references(_DomainObject, _Primitive, Refs) -> + Refs. + +check_reference_type(Object, Type, Refs) -> + case is_reference_type(Type) of + {true, Tag} -> + [{Tag, Object} | Refs]; + false -> + references(Object, Type, Refs) + end. + +is_reference_type(Type) -> + {struct, union, StructInfo} = get_struct_info('Reference'), + is_reference_type(Type, StructInfo). + +is_reference_type(_Type, []) -> + false; +is_reference_type(Type, [{_, _, Type, Tag, _} | _Rest]) -> + {true, Tag}; +is_reference_type(Type, [_ | Rest]) -> + is_reference_type(Type, Rest). + +indexfold(Fun, Acc, I, [E | Rest]) -> + indexfold(Fun, Fun(I, E, Acc), I + 1, Rest); +indexfold(_Fun, Acc, _I, []) -> + Acc. + +%% Common + +get_struct_info(StructName) -> + dmsl_domain_thrift:struct_info(StructName). + +get_field_info(Field, {struct, _StructType, FieldsInfo}) -> + lists:keyfind(Field, 4, FieldsInfo). diff --git a/apps/dmt_object/src/dmt_object_type.erl b/apps/dmt_object/src/dmt_object_type.erl new file mode 100644 index 0000000..e2373e8 --- /dev/null +++ b/apps/dmt_object/src/dmt_object_type.erl @@ -0,0 +1,23 @@ +-module(dmt_object_type). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +%% API +-export([get_refless_object_type/1]). +-export([get_ref_type/1]). + +get_refless_object_type(#domain_Category{}) -> + category; +get_refless_object_type(#domain_Currency{}) -> + currency; +get_refless_object_type(_) -> + error(not_impl). + +get_ref_type(#domain_CurrencyRef{}) -> + currency; +get_ref_type(#domain_CategoryRef{}) -> + category; +get_ref_type(undefined) -> + undefined; +get_ref_type(_) -> + error(not_impl). diff --git a/compose.tracing.yaml b/compose.tracing.yaml new file mode 100644 index 0000000..4ff5555 --- /dev/null +++ b/compose.tracing.yaml @@ -0,0 +1,27 @@ +services: + testrunner: + depends_on: + jaeger: + condition: service_healthy + environment: + - OTEL_SERVICE_NAME=dominant-v2 + - OTEL_TRACES_EXPORTER=otlp + - OTEL_TRACES_SAMPLER=parentbased_always_on + - OTEL_EXPORTER_OTLP_PROTOCOL=http_protobuf + - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318 + + jaeger: + image: jaegertracing/all-in-one:1.47 + environment: + - COLLECTOR_OTLP_ENABLED=true + healthcheck: + test: "/go/bin/all-in-one-linux status" + interval: 2s + timeout: 1s + retries: 20 + ports: + - 4317:4317 # OTLP gRPC receiver + - 4318:4318 # OTLP http receiver + - 5778:5778 + - 14250:14250 + - 16686:16686 diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..93d3f56 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,37 @@ +services: + + testrunner: + image: $DEV_IMAGE_TAG + environment: + WORK_DIR: $PWD + POSTGRES_HOST: db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: dmtv2 + build: + dockerfile: Dockerfile.dev + context: . + args: + OTP_VERSION: $OTP_VERSION + THRIFT_VERSION: $THRIFT_VERSION + volumes: + - .:$PWD + hostname: dominant-v2 + depends_on: + db: + condition: service_healthy + working_dir: $PWD + + db: + image: postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: dmt + ports: + - 5432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 diff --git a/config/sys.config b/config/sys.config new file mode 100644 index 0000000..0fae252 --- /dev/null +++ b/config/sys.config @@ -0,0 +1,59 @@ +[ + {kernel, [ + {log_level, info}, + {logger, [ + {handler, default, logger_std_h, #{ + level => + debug, + config => #{ + type => + {file, "/var/log/dominant-v2/console.json"}, + sync_mode_qlen => + 20 + }, + formatter => + {logger_logstash_formatter, #{}} + }} + ]} + ]}, + + {dmt, []}, + + {epg_connector, [ + {databases, #{ + default_db => #{ + host => "db", + port => 5432, + username => "postgres", + password => "postgres", + database => "dmt" + } + }}, + {pools, #{ + default_pool => #{ + database => default_db, + size => 10 + } + }} + ]}, + + {scoper, [ + {storage, scoper_storage_logger} + ]}, + + {how_are_you, [ + {metrics_publishers, [ + % {hay_statsd_publisher, #{ + % key_prefix => <<"dominant-v2.">>, + % host => "localhost", + % port => 8125 + % }} + ]} + ]}, + + {prometheus, [ + {collectors, [ + default + ]} + ]} +]. diff --git a/config/vm.args b/config/vm.args new file mode 100644 index 0000000..2a575fb --- /dev/null +++ b/config/vm.args @@ -0,0 +1,6 @@ +-sname dmt + +-setcookie dmt + ++K true ++A 10 diff --git a/migrations/1722105006-create_initial_tables.sql b/migrations/1722105006-create_initial_tables.sql new file mode 100644 index 0000000..774a073 --- /dev/null +++ b/migrations/1722105006-create_initial_tables.sql @@ -0,0 +1,515 @@ +-- migrations/1722105006-create_initial_tables.sql +-- :up +-- Up migration + +CREATE TABLE op_user ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE TABLE global_version ( + version BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID REFERENCES op_user(id) +); + +CREATE TABLE category ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE currency ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE business_schedule ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE calendar ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE payment_method ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE payout_method ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE bank ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE contract_template ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE term_set_hierarchy ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE payment_institution ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE provider ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE terminal ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE inspector ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE system_account_set ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE external_account_set ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE proxy ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE globals ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE cash_register_provider ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE routing_rules ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE bank_card_category ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE criterion ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE document_type ( + id TEXT NOT NULL, + sequence BIGINT, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE payment_service ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE payment_system ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE bank_card_token_service ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE mobile_operator ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE crypto_currency ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE country ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE trade_bloc ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE identity_provider ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE TABLE limit_config ( + id TEXT NOT NULL, + global_version BIGINT NOT NULL REFERENCES global_version(version), + references_to TEXT[] NOT NULL, + referenced_by TEXT[] NOT NULL, + data TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + is_active BOOLEAN DEFAULT TRUE, + PRIMARY KEY (id, global_version) +); + +CREATE INDEX idx_category_global_version ON category(global_version); +CREATE INDEX idx_currency_global_version ON currency(global_version); +CREATE INDEX idx_business_schedule_global_version ON business_schedule(global_version); +CREATE INDEX idx_calendar_global_version ON calendar(global_version); +CREATE INDEX idx_payment_method_global_version ON payment_method(global_version); +CREATE INDEX idx_payout_method_global_version ON payout_method(global_version); +CREATE INDEX idx_bank_global_version ON bank(global_version); +CREATE INDEX idx_contract_template_global_version ON contract_template(global_version); +CREATE INDEX idx_term_set_hierarchy_global_version ON term_set_hierarchy(global_version); +CREATE INDEX idx_payment_institution_global_version ON payment_institution(global_version); +CREATE INDEX idx_provider_global_version ON provider(global_version); +CREATE INDEX idx_terminal_global_version ON terminal(global_version); +CREATE INDEX idx_inspector_global_version ON inspector(global_version); +CREATE INDEX idx_system_account_set_global_version ON system_account_set(global_version); +CREATE INDEX idx_external_account_set_global_version ON external_account_set(global_version); +CREATE INDEX idx_proxy_global_version ON proxy(global_version); +CREATE INDEX idx_globals_global_version ON globals(global_version); +CREATE INDEX idx_cash_register_provider_global_version ON cash_register_provider(global_version); +CREATE INDEX idx_routing_rules_global_version ON routing_rules(global_version); +CREATE INDEX idx_bank_card_category_global_version ON bank_card_category(global_version); +CREATE INDEX idx_criterion_global_version ON criterion(global_version); +CREATE INDEX idx_document_type_global_version ON document_type(global_version); +CREATE INDEX idx_payment_service_global_version ON payment_service(global_version); +CREATE INDEX idx_payment_system_global_version ON payment_system(global_version); +CREATE INDEX idx_bank_card_token_service_global_version ON bank_card_token_service(global_version); +CREATE INDEX idx_mobile_operator_global_version ON mobile_operator(global_version); +CREATE INDEX idx_crypto_currency_global_version ON crypto_currency(global_version); +CREATE INDEX idx_country_global_version ON country(global_version); +CREATE INDEX idx_trade_bloc_global_version ON trade_bloc(global_version); +CREATE INDEX idx_identity_provider_global_version ON identity_provider(global_version); +CREATE INDEX idx_limit_config_global_version ON limit_config(global_version); + +CREATE INDEX idx_category_sequence ON category(sequence); +CREATE INDEX idx_business_schedule_sequence ON business_schedule(sequence); +CREATE INDEX idx_calendar_sequence ON calendar(sequence); +CREATE INDEX idx_bank_sequence ON bank(sequence); +CREATE INDEX idx_contract_template_sequence ON contract_template(sequence); +CREATE INDEX idx_term_set_hierarchy_sequence ON term_set_hierarchy(sequence); +CREATE INDEX idx_payment_institution_sequence ON payment_institution(sequence); +CREATE INDEX idx_provider_sequence ON provider(sequence); +CREATE INDEX idx_terminal_sequence ON terminal(sequence); +CREATE INDEX idx_inspector_sequence ON inspector(sequence); +CREATE INDEX idx_system_account_set_sequence ON system_account_set(sequence); +CREATE INDEX idx_external_account_set_sequence ON external_account_set(sequence); +CREATE INDEX idx_proxy_sequence ON proxy(sequence); +CREATE INDEX idx_cash_register_provider_sequence ON cash_register_provider(sequence); +CREATE INDEX idx_routing_rules_sequence ON routing_rules(sequence); +CREATE INDEX idx_bank_card_category_sequence ON bank_card_category(sequence); +CREATE INDEX idx_criterion_sequence ON criterion(sequence); +CREATE INDEX idx_document_type_sequence ON document_type(sequence); +-- :down +-- Down migration + +DROP INDEX IF EXISTS idx_category_global_version; +DROP INDEX IF EXISTS idx_currency_global_version; +DROP INDEX IF EXISTS idx_business_schedule_global_version; +DROP INDEX IF EXISTS idx_calendar_global_version; +DROP INDEX IF EXISTS idx_payment_method_global_version; +DROP INDEX IF EXISTS idx_payout_method_global_version; +DROP INDEX IF EXISTS idx_bank_global_version; +DROP INDEX IF EXISTS idx_contract_template_global_version; +DROP INDEX IF EXISTS idx_term_set_hierarchy_global_version; +DROP INDEX IF EXISTS idx_payment_institution_global_version; +DROP INDEX IF EXISTS idx_provider_global_version; +DROP INDEX IF EXISTS idx_terminal_global_version; +DROP INDEX IF EXISTS idx_inspector_global_version; +DROP INDEX IF EXISTS idx_system_account_set_global_version; +DROP INDEX IF EXISTS idx_external_account_set_global_version; +DROP INDEX IF EXISTS idx_proxy_global_version; +DROP INDEX IF EXISTS idx_globals_global_version; +DROP INDEX IF EXISTS idx_cash_register_provider_global_version; +DROP INDEX IF EXISTS idx_routing_rules_global_version; +DROP INDEX IF EXISTS idx_bank_card_category_global_version; +DROP INDEX IF EXISTS idx_criterion_global_version; +DROP INDEX IF EXISTS idx_document_type_global_version; +DROP INDEX IF EXISTS idx_payment_service_global_version; +DROP INDEX IF EXISTS idx_payment_system_global_version; +DROP INDEX IF EXISTS idx_bank_card_token_service_global_version; +DROP INDEX IF EXISTS idx_mobile_operator_global_version; +DROP INDEX IF EXISTS idx_crypto_currency_global_version; +DROP INDEX IF EXISTS idx_country_global_version; +DROP INDEX IF EXISTS idx_trade_bloc_global_version; +DROP INDEX IF EXISTS idx_identity_provider_global_version; +DROP INDEX IF EXISTS idx_limit_config_global_version; + +DROP INDEX IF EXISTS idx_category_sequence; +DROP INDEX IF EXISTS idx_business_schedule_sequence; +DROP INDEX IF EXISTS idx_calendar_sequence; +DROP INDEX IF EXISTS idx_bank_sequence; +DROP INDEX IF EXISTS idx_contract_template_sequence; +DROP INDEX IF EXISTS idx_term_set_hierarchy_sequence; +DROP INDEX IF EXISTS idx_payment_institution_sequence; +DROP INDEX IF EXISTS idx_provider_sequence; +DROP INDEX IF EXISTS idx_terminal_sequence; +DROP INDEX IF EXISTS idx_inspector_sequence; +DROP INDEX IF EXISTS idx_system_account_set_sequence; +DROP INDEX IF EXISTS idx_external_account_set_sequence; +DROP INDEX IF EXISTS idx_proxy_sequence; +DROP INDEX IF EXISTS idx_cash_register_provider_sequence; +DROP INDEX IF EXISTS idx_routing_rules_sequence; +DROP INDEX IF EXISTS idx_bank_card_category_sequence; +DROP INDEX IF EXISTS idx_criterion_sequence; +DROP INDEX IF EXISTS idx_document_type_sequence; + +DROP TABLE IF EXISTS category; +DROP TABLE IF EXISTS currency; +DROP TABLE IF EXISTS business_schedule; +DROP TABLE IF EXISTS calendar; +DROP TABLE IF EXISTS payment_method; +DROP TABLE IF EXISTS payout_method; +DROP TABLE IF EXISTS bank; +DROP TABLE IF EXISTS contract_template; +DROP TABLE IF EXISTS term_set_hierarchy; +DROP TABLE IF EXISTS payment_institution; +DROP TABLE IF EXISTS provider; +DROP TABLE IF EXISTS terminal; +DROP TABLE IF EXISTS inspector; +DROP TABLE IF EXISTS system_account_set; +DROP TABLE IF EXISTS external_account_set; +DROP TABLE IF EXISTS proxy; +DROP TABLE IF EXISTS globals; +DROP TABLE IF EXISTS cash_register_provider; +DROP TABLE IF EXISTS routing_rules; +DROP TABLE IF EXISTS bank_card_category; +DROP TABLE IF EXISTS criterion; +DROP TABLE IF EXISTS document_type; +DROP TABLE IF EXISTS payment_service; +DROP TABLE IF EXISTS payment_system; +DROP TABLE IF EXISTS bank_card_token_service; +DROP TABLE IF EXISTS mobile_operator; +DROP TABLE IF EXISTS crypto_currency; +DROP TABLE IF EXISTS country; +DROP TABLE IF EXISTS trade_bloc; +DROP TABLE IF EXISTS identity_provider; +DROP TABLE IF EXISTS limit_config; + +DROP TABLE IF EXISTS global_version; + +DROP TABLE IF EXISTS op_user; diff --git a/psql-migration b/psql-migration new file mode 160000 index 0000000..c84b06f --- /dev/null +++ b/psql-migration @@ -0,0 +1 @@ +Subproject commit c84b06fc7e1603783eb81eaad214552151ed9066 diff --git a/rebar.config b/rebar.config index cb9b2eb..fa64ef1 100644 --- a/rebar.config +++ b/rebar.config @@ -1,7 +1,113 @@ -{erl_opts, [debug_info]}. -{deps, []}. +%% Common project erlang options. +{erl_opts, [ + % mandatory + %% debug_info, + %% warnings_as_errors, + %% warn_export_all, + %% warn_missing_spec, + %% warn_untyped_record, + %% warn_export_vars, + + % by default + %% warn_unused_record, + %% warn_bif_clash, + %% warn_obsolete_guard, + %% warn_unused_vars, + %% warn_shadow_vars, + %% warn_unused_import, + %% warn_unused_function, + %% warn_deprecated_function + + % at will + % bin_opt_info + % no_auto_import + % warn_missing_spec_all +]}. + +% Common project dependencies. +{deps, [ + {genlib, {git, "https://github.com/valitydev/genlib.git", {branch, "master"}}}, + {cowboy_draining_server, {git, "https://github.com/valitydev/cowboy_draining_server.git", {branch, "master"}}}, + {uuid, {git, "https://github.com/okeuday/uuid.git", {branch, "master"}}}, + {scoper, {git, "https://github.com/valitydev/scoper.git", {branch, "master"}}}, + {erl_health, {git, "https://github.com/valitydev/erlang-health.git", {branch, "master"}}}, + {cowboy_cors, {git, "https://github.com/valitydev/cowboy_cors.git", {branch, master}}}, + {cowboy_access_log, {git, "https://github.com/valitydev/cowboy_access_log.git", {branch, "master"}}}, + {woody_user_identity, {git, "https://github.com/valitydev/woody_erlang_user_identity.git", {branch, "master"}}}, + {woody, {git, "https://github.com/valitydev/woody_erlang.git", {branch, master}}}, + {damsel, {git, "git@github.com:valitydev/damsel.git", {branch, "IMP-281/dmt_proto"}}}, + + %% Libraries for postgres interaction + {epg_connector, {git, "git@github.com:valitydev/epg_connector.git", {branch, master}}}, + {epgsql, {git, "https://github.com/epgsql/epgsql.git", {tag, "4.7.1"}}}, + {epgsql_pool, {git, "https://github.com/wgnet/epgsql_pool", {branch, "master"}}}, + {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, + + %% For db migrations + {envloader, {git, "https://github.com/nuex/envloader.git", {branch, "master"}}}, + eql, + getopt, + + {prometheus, "4.6.0"}, + {prometheus_cowboy, "0.1.8"}, + + %% OpenTelemetry deps + {opentelemetry_api, "1.2.1"}, + {opentelemetry, "1.3.0"}, + {opentelemetry_exporter, "1.3.0"} +]}. + +%% XRef checks +{xref_checks, [ + undefined_function_calls, + undefined_functions, + deprecated_functions_calls, + deprecated_functions +]}. + +%% Dialyzer static analyzing +{dialyzer, [ + {warnings, [ + % mandatory + unmatched_returns, + error_handling, + % race_conditions, + unknown + ]}, + {plt_apps, all_deps}, + {incremental, true} +]}. + +{project_plugins, [ + {rebar3_lint, "3.2.6"}, + {erlfmt, "1.5.0"}, + {covertool, "2.0.6"} +]}. + +%% Linter config. +{elvis_output_format, colors}. + +{erlfmt, [ + {print_width, 120}, + {files, [ + "apps/dmt*/{src,include,test}/*.{hrl,erl,app.src}", + "rebar.config", + "elvis.config", + "config/sys.config", + "test/*/sys.config" + ]} +]}. + +{covertool, [ + {coverdata_files, [ + "eunit.coverdata", + "ct.coverdata" + ]} +]}. {shell, [ - % {config, "config/sys.config"}, - {apps, [dominant-v2]} + {config, "config/sys.config"}, + {apps, [ + dmt + ]} ]}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..bead105 --- /dev/null +++ b/rebar.lock @@ -0,0 +1,191 @@ +{"1.2.0", +[{<<"accept">>,{pkg,<<"accept">>,<<"0.3.5">>},2}, + {<<"acceptor_pool">>,{pkg,<<"acceptor_pool">>,<<"1.0.0">>},2}, + {<<"cache">>,{pkg,<<"cache">>,<<"2.3.3">>},1}, + {<<"canal">>, + {git,"https://github.com/valitydev/canal", + {ref,"621d3821cd0a6036fee75d8e3b2d17167f3268e4"}}, + 1}, + {<<"certifi">>,{pkg,<<"certifi">>,<<"2.8.0">>},2}, + {<<"cg_mon">>, + {git,"https://github.com/rbkmoney/cg_mon.git", + {ref,"5a87a37694e42b6592d3b4164ae54e0e87e24e18"}}, + 1}, + {<<"chatterbox">>,{pkg,<<"ts_chatterbox">>,<<"0.15.1">>},2}, + {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.9.0">>},1}, + {<<"cowboy_access_log">>, + {git,"https://github.com/valitydev/cowboy_access_log.git", + {ref,"04da359e022cf05c5c93812504d5791d6bc97453"}}, + 0}, + {<<"cowboy_cors">>, + {git,"https://github.com/valitydev/cowboy_cors.git", + {ref,"5a3b084fb8c5a4ff58e3c915a822d709d6023c3b"}}, + 0}, + {<<"cowboy_draining_server">>, + {git,"https://github.com/valitydev/cowboy_draining_server.git", + {ref,"186cf4d0722d4ad79afe73d371df6b1371e51905"}}, + 0}, + {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},2}, + {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, + {<<"damsel">>, + {git,"git@github.com:valitydev/damsel.git", + {ref,"de7ce44874984331f8b180bef3a786bd35573e48"}}, + 0}, + {<<"envloader">>, + {git,"https://github.com/nuex/envloader.git", + {ref,"27a97e04f35c554995467b9236d8ae0188d468c7"}}, + 0}, + {<<"epg_connector">>, + {git,"git@github.com:valitydev/epg_connector.git", + {ref,"7fc3aa1b6d9c8be69a64fefd18f6aaa416dcd572"}}, + 0}, + {<<"epgsql">>, + {git,"https://github.com/epgsql/epgsql.git", + {ref,"7ba52768cf0ea7d084df24d4275a88eef4db13c2"}}, + 0}, + {<<"epgsql_pool">>, + {git,"https://github.com/wgnet/epgsql_pool", + {ref,"f5e492f73752950aab932a1662536e22fc00c717"}}, + 0}, + {<<"eql">>,{pkg,<<"eql">>,<<"0.2.0">>},0}, + {<<"erl_health">>, + {git,"https://github.com/valitydev/erlang-health.git", + {ref,"49716470d0e8dab5e37db55d52dea78001735a3d"}}, + 0}, + {<<"genlib">>, + {git,"https://github.com/valitydev/genlib.git", + {ref,"f6074551d6586998e91a97ea20acb47241254ff3"}}, + 0}, + {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.3">>},0}, + {<<"gproc">>,{pkg,<<"gproc">>,<<"0.9.0">>},1}, + {<<"grpcbox">>,{pkg,<<"grpcbox">>,<<"0.17.1">>},1}, + {<<"gun">>, + {git,"https://github.com/ninenines/gun.git", + {ref,"e7dd9f227e46979d8073e71c683395a809b78cb4"}}, + 1}, + {<<"hackney">>,{pkg,<<"hackney">>,<<"1.18.0">>},1}, + {<<"herd">>, + {git,"https://github.com/wgnet/herd.git", + {ref,"934847589dcf5a6d2b02a1f546ffe91c04066f17"}}, + 0}, + {<<"hpack">>,{pkg,<<"hpack_erl">>,<<"0.3.0">>},3}, + {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},2}, + {<<"jsone">>,{pkg,<<"jsone">>,<<"1.8.0">>},2}, + {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, + {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, + {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.3.0">>},2}, + {<<"opentelemetry">>,{pkg,<<"opentelemetry">>,<<"1.3.0">>},0}, + {<<"opentelemetry_api">>,{pkg,<<"opentelemetry_api">>,<<"1.2.1">>},0}, + {<<"opentelemetry_exporter">>, + {pkg,<<"opentelemetry_exporter">>,<<"1.3.0">>}, + 0}, + {<<"opentelemetry_semantic_conventions">>, + {pkg,<<"opentelemetry_semantic_conventions">>,<<"0.2.0">>}, + 1}, + {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.3.1">>},2}, + {<<"pooler">>,{pkg,<<"pooler">>,<<"1.5.3">>},1}, + {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.6.0">>},0}, + {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.8">>},0}, + {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.11">>},1}, + {<<"quickrand">>, + {git,"https://github.com/okeuday/quickrand.git", + {ref,"65332de501998764f437c3ffe05d744f582d7622"}}, + 1}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"1.8.0">>},2}, + {<<"scoper">>, + {git,"https://github.com/valitydev/scoper.git", + {ref,"55a2a32ee25e22fa35f583a18eaf38b2b743429b"}}, + 0}, + {<<"snowflake">>, + {git,"https://github.com/valitydev/snowflake.git", + {ref,"de159486ef40cec67074afe71882bdc7f7deab72"}}, + 1}, + {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, + {<<"thrift">>, + {git,"https://github.com/valitydev/thrift_erlang.git", + {ref,"c280ff266ae1c1906fb0dcee8320bb8d8a4a3c75"}}, + 1}, + {<<"tls_certificate_check">>, + {pkg,<<"tls_certificate_check">>,<<"1.23.0">>}, + 1}, + {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.0">>},2}, + {<<"uuid">>, + {git,"https://github.com/okeuday/uuid.git", + {ref,"c65307d7991e11dff96064dadf6880108ac0d911"}}, + 0}, + {<<"woody">>, + {git,"https://github.com/valitydev/woody_erlang.git", + {ref,"072825ee7179825a4078feb0649df71303c74157"}}, + 0}, + {<<"woody_user_identity">>, + {git,"https://github.com/valitydev/woody_erlang_user_identity.git", + {ref,"a480762fea8d7c08f105fb39ca809482b6cb042e"}}, + 0}]}. +[ +{pkg_hash,[ + {<<"accept">>, <<"B33B127ABCA7CC948BBE6CAA4C263369ABF1347CFA9D8E699C6D214660F10CD1">>}, + {<<"acceptor_pool">>, <<"43C20D2ACAE35F0C2BCD64F9D2BDE267E459F0F3FD23DAB26485BF518C281B21">>}, + {<<"cache">>, <<"B23A5FE7095445A88412A6E614C933377E0137B44FFED77C9B3FEF1A731A20B2">>}, + {<<"certifi">>, <<"D4FB0A6BB20B7C9C3643E22507E42F356AC090A1DCEA9AB99E27E0376D695EBA">>}, + {<<"chatterbox">>, <<"5CAC4D15DD7AD61FC3C4415CE4826FC563D4643DEE897A558EC4EA0B1C835C9C">>}, + {<<"cowboy">>, <<"865DD8B6607E14CF03282E10E934023A1BD8BE6F6BACF921A7E2A96D800CD452">>}, + {<<"cowlib">>, <<"0B9FF9C346629256C42EBE1EEB769A83C6CB771A6EE5960BD110AB0B9B872063">>}, + {<<"ctx">>, <<"8FF88B70E6400C4DF90142E7F130625B82086077A45364A78D208ED3ED53C7FE">>}, + {<<"eql">>, <<"598ABC19A1CF6AFB8EF89FFEA869F43BAEBB1CEC3260DD5065112FEE7D8CE3E2">>}, + {<<"getopt">>, <<"4F3320C1F6F26B2BEC0F6C6446B943EB927A1E6428EA279A1C6C534906EE79F1">>}, + {<<"gproc">>, <<"853CCB7805E9ADA25D227A157BA966F7B34508F386A3E7E21992B1B484230699">>}, + {<<"grpcbox">>, <<"6E040AB3EF16FE699FFB513B0EF8E2E896DA7B18931A1EF817143037C454BCCE">>}, + {<<"hackney">>, <<"C4443D960BB9FBA6D01161D01CD81173089686717D9490E5D3606644C48D121F">>}, + {<<"hpack">>, <<"2461899CC4AB6A0EF8E970C1661C5FC6A52D3C25580BC6DD204F84CE94669926">>}, + {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, + {<<"jsone">>, <<"347FF1FA700E182E1F9C5012FA6D737B12C854313B9AE6954CA75D3987D6C06D">>}, + {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, + {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, + {<<"mimerl">>, <<"D0CD9FC04B9061F82490F6581E0128379830E78535E017F7780F37FEA7545726">>}, + {<<"opentelemetry">>, <<"988AC3C26ACAC9720A1D4FB8D9DC52E95B45ECFEC2D5B5583276A09E8936BC5E">>}, + {<<"opentelemetry_api">>, <<"7B69ED4F40025C005DE0B74FCE8C0549625D59CB4DF12D15C32FE6DC5076FF42">>}, + {<<"opentelemetry_exporter">>, <<"1D8809C0D4F4ACF986405F7700ED11992BCBDB6A4915DD11921E80777FFA7167">>}, + {<<"opentelemetry_semantic_conventions">>, <<"B67FE459C2938FCAB341CB0951C44860C62347C005ACE1B50F8402576F241435">>}, + {<<"parse_trans">>, <<"16328AB840CC09919BD10DAB29E431DA3AF9E9E7E7E6F0089DD5A2D2820011D8">>}, + {<<"pooler">>, <<"898CD1FA301FC42D4A8ED598CE139B71CA85B54C16AB161152B5CC5FBDCFA1A8">>}, + {<<"prometheus">>, <<"20510F381DB1CCAB818B4CF2FAC5FA6AB5CC91BC364A154399901C001465F46F">>}, + {<<"prometheus_cowboy">>, <<"CFCE0BC7B668C5096639084FCD873826E6220EA714BF60A716F5BD080EF2A99C">>}, + {<<"prometheus_httpd">>, <<"F616ED9B85B536B195D94104063025A91F904A4CFC20255363F49A197D96C896">>}, + {<<"ranch">>, <<"8C7A100A139FD57F17327B6413E4167AC559FBC04CA7448E9BE9057311597A1D">>}, + {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}, + {<<"tls_certificate_check">>, <<"BB7869C629DE4EC72D4652520C1AD2255BB5712AD09A6568C41B0294B3CEC78F">>}, + {<<"unicode_util_compat">>, <<"BC84380C9AB48177092F43AC89E4DFA2C6D62B40B8BD132B1059ECC7232F9A78">>}]}, +{pkg_hash_ext,[ + {<<"accept">>, <<"11B18C220BCC2EAB63B5470C038EF10EB6783BCB1FCDB11AA4137DEFA5AC1BB8">>}, + {<<"acceptor_pool">>, <<"0CBCD83FDC8B9AD2EEE2067EF8B91A14858A5883CB7CD800E6FCD5803E158788">>}, + {<<"cache">>, <<"44516CE6FA03594D3A2AF025DD3A87BFE711000EB730219E1DDEFC816E0AA2F4">>}, + {<<"certifi">>, <<"6AC7EFC1C6F8600B08D625292D4BBF584E14847CE1B6B5C44D983D273E1097EA">>}, + {<<"chatterbox">>, <<"4F75B91451338BC0DA5F52F3480FA6EF6E3A2AEECFC33686D6B3D0A0948F31AA">>}, + {<<"cowboy">>, <<"2C729F934B4E1AA149AFF882F57C6372C15399A20D54F65C8D67BEF583021BDE">>}, + {<<"cowlib">>, <<"2B3E9DA0B21C4565751A6D4901C20D1B4CC25CBB7FD50D91D2AB6DD287BC86A9">>}, + {<<"ctx">>, <<"A14ED2D1B67723DBEBBE423B28D7615EB0BDCBA6FF28F2D1F1B0A7E1D4AA5FC2">>}, + {<<"eql">>, <<"513BE6B36EE86E8292B2B7475C257ABB66CED5AAD40CBF7AD21E233D0A3BF51D">>}, + {<<"getopt">>, <<"7E01DE90AC540F21494FF72792B1E3162D399966EBBFC674B4CE52CB8F49324F">>}, + {<<"gproc">>, <<"587E8AF698CCD3504CF4BA8D90F893EDE2B0F58CABB8A916E2BF9321DE3CF10B">>}, + {<<"grpcbox">>, <<"4A3B5D7111DAABC569DC9CBD9B202A3237D81C80BF97212FBC676832CB0CEB17">>}, + {<<"hackney">>, <<"9AFCDA620704D720DB8C6A3123E9848D09C87586DC1C10479C42627B905B5C5E">>}, + {<<"hpack">>, <<"D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0">>}, + {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, + {<<"jsone">>, <<"08560B78624A12E0B5E7EC0271EC8CA38EF51F63D84D84843473E14D9B12618C">>}, + {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, + {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, + {<<"mimerl">>, <<"A1E15A50D1887217DE95F0B9B0793E32853F7C258A5CD227650889B38839FE9D">>}, + {<<"opentelemetry">>, <<"8E09EDC26AAD11161509D7ECAD854A3285D88580F93B63B0B1CF0BAC332BFCC0">>}, + {<<"opentelemetry_api">>, <<"6D7A27B7CAD2AD69A09CABF6670514CAFCEC717C8441BEB5C96322BAC3D05350">>}, + {<<"opentelemetry_exporter">>, <<"2B40007F509D38361744882FD060A8841AF772AB83BB542AA5350908B303AD65">>}, + {<<"opentelemetry_semantic_conventions">>, <<"D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895">>}, + {<<"parse_trans">>, <<"07CD9577885F56362D414E8C4C4E6BDF10D43A8767ABB92D24CBE8B24C54888B">>}, + {<<"pooler">>, <<"058D85C5081289B90E97E4DDDBC3BB5A3B4A19A728AB3BC88C689EFCC36A07C7">>}, + {<<"prometheus">>, <<"4905FD2992F8038ECCD7AA0CD22F40637ED618C0BED1F75C05AACEC15B7545DE">>}, + {<<"prometheus_cowboy">>, <<"BA286BECA9302618418892D37BCD5DC669A6CC001F4EB6D6AF85FF81F3F4F34C">>}, + {<<"prometheus_httpd">>, <<"0BBE831452CFDF9588538EB2F570B26F30C348ADAE5E95A7D87F35A5910BCF92">>}, + {<<"ranch">>, <<"49FBCFD3682FAB1F5D109351B61257676DA1A2FDBE295904176D5E521A2DDFE5">>}, + {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}, + {<<"tls_certificate_check">>, <<"79D0C84EFFC7C81AC1E85FA38B1C33572FE2976FB8FAFDFB2F0140DE0442D494">>}, + {<<"unicode_util_compat">>, <<"25EEE6D67DF61960CF6A794239566599B09E17E668D3700247BC498638152521">>}]} +]. diff --git a/src/dominant-v2.app.src b/src/dominant-v2.app.src deleted file mode 100644 index 97815c2..0000000 --- a/src/dominant-v2.app.src +++ /dev/null @@ -1,15 +0,0 @@ -{application, dominant-v2, - [{description, "An OTP application"}, - {vsn, "0.1.0"}, - {registered, []}, - {mod, {dominant-v2_app, []}}, - {applications, - [kernel, - stdlib - ]}, - {env,[]}, - {modules, []}, - - {licenses, ["Apache-2.0"]}, - {links, []} - ]}. diff --git a/src/dominant-v2_sup.erl b/src/dominant-v2_sup.erl deleted file mode 100644 index bd152bf..0000000 --- a/src/dominant-v2_sup.erl +++ /dev/null @@ -1,35 +0,0 @@ -%%%------------------------------------------------------------------- -%% @doc dominant-v2 top level supervisor. -%% @end -%%%------------------------------------------------------------------- - --module(dominant-v2_sup). - --behaviour(supervisor). - --export([start_link/0]). - --export([init/1]). - --define(SERVER, ?MODULE). - -start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). - -%% sup_flags() = #{strategy => strategy(), % optional -%% intensity => non_neg_integer(), % optional -%% period => pos_integer()} % optional -%% child_spec() = #{id => child_id(), % mandatory -%% start => mfargs(), % mandatory -%% restart => restart(), % optional -%% shutdown => shutdown(), % optional -%% type => worker(), % optional -%% modules => modules()} % optional -init([]) -> - SupFlags = #{strategy => one_for_all, - intensity => 0, - period => 1}, - ChildSpecs = [], - {ok, {SupFlags, ChildSpecs}}. - -%% internal functions