mirror of
https://github.com/valitydev/dominant-v2.git
synced 2024-11-06 00:35:21 +00:00
IMP-284: Implement new DB schema (#1)
* IMP-284: Implement new DB schema * WIP * WIP * SQL Request for commiting * Implement RepositoryClient and UserOpManagement * Raw integration tests * WIP Testing * WIP Testing * Tests WIP * Insert objects rebuild * Remade sql requests and make test work * Extra tests * Extra tests * Update/Delete work * Rename everything from dmt_v2 to dmt
This commit is contained in:
parent
57aab45c92
commit
604d1b5b78
5
.env
Normal file
5
.env
Normal file
@ -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
|
36
.gitignore
vendored
36
.gitignore
vendored
@ -1,20 +1,20 @@
|
|||||||
.rebar3
|
# general
|
||||||
_build
|
|
||||||
_checkouts
|
|
||||||
_vendor
|
|
||||||
.eunit
|
|
||||||
*.o
|
|
||||||
*.beam
|
|
||||||
*.plt
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
.erlang.cookie
|
|
||||||
ebin
|
|
||||||
log
|
log
|
||||||
erl_crash.dump
|
/_build/
|
||||||
.rebar
|
/_checkouts/
|
||||||
logs
|
|
||||||
.idea
|
|
||||||
*.iml
|
|
||||||
rebar3.crashdump
|
|
||||||
*~
|
*~
|
||||||
|
erl_crash.dump
|
||||||
|
rebar3.crashdump
|
||||||
|
.tags*
|
||||||
|
*.sublime-workspace
|
||||||
|
.edts
|
||||||
|
.DS_Store
|
||||||
|
/.idea/
|
||||||
|
*.beam
|
||||||
|
/test/log/
|
||||||
|
|
||||||
|
tags
|
||||||
|
.image.dev
|
||||||
|
bin
|
||||||
|
|
||||||
|
.codestract
|
||||||
|
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[submodule "psql-migration"]
|
||||||
|
path = psql-migration
|
||||||
|
url = git@github.com:valitydev/psql-migration.git
|
||||||
|
branch = get_rid_of_binary
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
erlang 27.1
|
||||||
|
rebar 3.23.0
|
59
Dockerfile
Normal file
59
Dockerfile
Normal file
@ -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
|
17
Dockerfile.dev
Normal file
17
Dockerfile.dev
Normal file
@ -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"]
|
119
Makefile
Normal file
119
Makefile
Normal file
@ -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
|
34
README.md
34
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 [<dir>]] [-e [<env>]] <command>
|
||||||
|
|
||||||
|
-h, --help Print this help text
|
||||||
|
-d, --dir Migration folder [default: migrations]
|
||||||
|
-e, --env Environment file to search for DATABASE_URL [default: .env]
|
||||||
|
new <name> 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.
|
||||||
|
```
|
||||||
|
30
apps/dmt/src/dmt.app.src
Normal file
30
apps/dmt/src/dmt.app.src
Normal file
@ -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, []}
|
||||||
|
]}.
|
43
apps/dmt/src/dmt_api_woody_utils.erl
Normal file
43
apps/dmt/src/dmt_api_woody_utils.erl
Normal file
@ -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.
|
@ -1,16 +1,16 @@
|
|||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%% @doc dominant-v2 public API
|
%% @doc dmt public API
|
||||||
%% @end
|
%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
-module(dominant-v2_app).
|
-module(dmt_app).
|
||||||
|
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
dominant-v2_sup:start_link().
|
dmt_sup:start_link().
|
||||||
|
|
||||||
stop(_State) ->
|
stop(_State) ->
|
||||||
ok.
|
ok.
|
332
apps/dmt/src/dmt_db_migration.erl
Normal file
332
apps/dmt/src/dmt_db_migration.erl
Normal file
@ -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.
|
654
apps/dmt/src/dmt_repository.erl
Normal file
654
apps/dmt/src/dmt_repository.erl
Normal file
@ -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).
|
59
apps/dmt/src/dmt_repository_client_handler.erl
Normal file
59
apps/dmt/src/dmt_repository_client_handler.erl
Normal file
@ -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.
|
61
apps/dmt/src/dmt_repository_handler.erl
Normal file
61
apps/dmt/src/dmt_repository_handler.erl
Normal file
@ -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
|
||||||
|
].
|
144
apps/dmt/src/dmt_sup.erl
Normal file
144
apps/dmt/src/dmt_sup.erl
Normal file
@ -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, []}.
|
50
apps/dmt/src/dmt_user_op.erl
Normal file
50
apps/dmt/src/dmt_user_op.erl
Normal file
@ -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.
|
45
apps/dmt/src/dmt_user_op_handler.erl
Normal file
45
apps/dmt/src/dmt_user_op_handler.erl
Normal file
@ -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.
|
46
apps/dmt/test/dmt_client.erl
Normal file
46
apps/dmt/test/dmt_client.erl
Normal file
@ -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).
|
38
apps/dmt/test/dmt_client_api.erl
Normal file
38
apps/dmt/test/dmt_client_api.erl
Normal file
@ -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.
|
169
apps/dmt/test/dmt_ct_helper.erl
Normal file
169
apps/dmt/test/dmt_ct_helper.erl
Normal file
@ -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.
|
371
apps/dmt/test/dmt_integration_test_SUITE.erl
Normal file
371
apps/dmt/test/dmt_integration_test_SUITE.erl
Normal file
@ -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.
|
14
apps/dmt_core/src/dmt_core.app.src
Normal file
14
apps/dmt_core/src/dmt_core.app.src
Normal file
@ -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, []}
|
||||||
|
]}.
|
155
apps/dmt_core/src/dmt_domain.erl
Normal file
155
apps/dmt_core/src/dmt_domain.erl
Normal file
@ -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).
|
92
apps/dmt_core/src/dmt_domain_pt.erl
Normal file
92
apps/dmt_core/src/dmt_domain_pt.erl
Normal file
@ -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)).
|
50
apps/dmt_core/src/dmt_history.erl
Normal file
50
apps/dmt_core/src/dmt_history.erl
Normal file
@ -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.
|
14
apps/dmt_object/src/dmt_object.app.src
Normal file
14
apps/dmt_object/src/dmt_object.app.src
Normal file
@ -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, []}
|
||||||
|
]}.
|
125
apps/dmt_object/src/dmt_object.erl
Normal file
125
apps/dmt_object/src/dmt_object.erl
Normal file
@ -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).
|
45
apps/dmt_object/src/dmt_object_id.erl
Normal file
45
apps/dmt_object/src/dmt_object_id.erl
Normal file
@ -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}).
|
156
apps/dmt_object/src/dmt_object_reference.erl
Normal file
156
apps/dmt_object/src/dmt_object_reference.erl
Normal file
@ -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).
|
23
apps/dmt_object/src/dmt_object_type.erl
Normal file
23
apps/dmt_object/src/dmt_object_type.erl
Normal file
@ -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).
|
27
compose.tracing.yaml
Normal file
27
compose.tracing.yaml
Normal file
@ -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
|
37
compose.yaml
Normal file
37
compose.yaml
Normal file
@ -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
|
59
config/sys.config
Normal file
59
config/sys.config
Normal file
@ -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
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
].
|
6
config/vm.args
Normal file
6
config/vm.args
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-sname dmt
|
||||||
|
|
||||||
|
-setcookie dmt
|
||||||
|
|
||||||
|
+K true
|
||||||
|
+A 10
|
515
migrations/1722105006-create_initial_tables.sql
Normal file
515
migrations/1722105006-create_initial_tables.sql
Normal file
@ -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;
|
1
psql-migration
Submodule
1
psql-migration
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit c84b06fc7e1603783eb81eaad214552151ed9066
|
114
rebar.config
114
rebar.config
@ -1,7 +1,113 @@
|
|||||||
{erl_opts, [debug_info]}.
|
%% Common project erlang options.
|
||||||
{deps, []}.
|
{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, [
|
{shell, [
|
||||||
% {config, "config/sys.config"},
|
{config, "config/sys.config"},
|
||||||
{apps, [dominant-v2]}
|
{apps, [
|
||||||
|
dmt
|
||||||
|
]}
|
||||||
]}.
|
]}.
|
||||||
|
191
rebar.lock
Normal file
191
rebar.lock
Normal file
@ -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">>}]}
|
||||||
|
].
|
@ -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, []}
|
|
||||||
]}.
|
|
@ -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
|
|
Loading…
Reference in New Issue
Block a user