Current section
Files
Jump to
Current section
Files
src/tivan_server.erl
%%%-------------------------------------------------------------------
%%% @author danny
%%% @copyright (C) 2019, danny
%%% @doc
%%%
%%% @end
%%% Created : 2019-05-01 09:43:53.183980
%%%-------------------------------------------------------------------
-module(tivan_server).
-behaviour(gen_server).
-callback init(Args :: list()) -> {'ok', State :: map()}.
-callback handle_info(Info :: term()) -> 'ok'.
-optional_callbacks([handle_info/1]).
-define(NATIVE_TYPES, [binary, list, tuple, atom, integer, float, second, millisecond, microsecond
,map ,nanosecond, uuid, pid, boolean]).
%% API
-export([start_link/4
,drop/2
,table_defs/1
,put/3
,put_s/3
,get/3
,get_s/3
,remove/3
,remove_s/3
,initialize/1]).
%% gen_server callbacks
-export([init/1
,handle_call/3
,handle_cast/2
,handle_info/2
,terminate/2
,code_change/3]).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link(Registration, Callback, Arguments, Options) ->
Server = case Registration of
{local, Name} -> Name;
{global, GlobalName} -> GlobalName;
{via, _Module, ViaName} -> ViaName
end,
gen_server:start_link(Registration, ?MODULE, [Callback, Server|Arguments], Options).
drop(Server, Table) ->
gen_server:cast(Server, {drop, Table}).
table_defs(Server) ->
gen_server:call(Server, table_defs).
put(Server, Table, Object) ->
TableDefs = persistent_term:get({Server, table_defs}),
do_put(Table, Object, TableDefs).
put_s(Server, Table, Object) ->
gen_server:call(Server, {put, Table, Object}).
get(Server, Table, Options) ->
TableDefs = persistent_term:get({Server, table_defs}),
do_get(Table, Options, TableDefs).
get_s(Server, Table, Options) ->
gen_server:call(Server, {get, Table, Options}).
remove(Server, Table, Object) ->
TableDefs = persistent_term:get({Server, table_defs}),
do_remove(Table, Object, TableDefs).
remove_s(Server, Table, Object) ->
gen_server:call(Server, {remove, Table, Object}).
initialize(Server) ->
gen_server:cast(Server, initialize).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([Callback, Server|Arguments]) ->
case Callback:init(Arguments) of
{ok, TableDefs} ->
TableDefsU = init_tables(TableDefs),
persistent_term:put({Server, table_defs}, TableDefsU),
{ok, #{callback => Callback, init_args => Arguments
,server => Server, table_defs => TableDefsU}};
Other ->
Other
end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%% {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call(table_defs, _From, #{table_defs := TableDefs} = State) ->
{reply, TableDefs, State};
handle_call({put, Table, Object}, _From, #{table_defs := TableDefs} = State) ->
Reply = do_put(Table, Object, TableDefs),
{reply, Reply, State};
handle_call({get, Table, Options}, _From, #{table_defs := TableDefs} = State) ->
Reply = do_get(Table, Options, TableDefs),
{reply, Reply, State};
handle_call({remove, Table, Object}, _From, #{table_defs := TableDefs} = State) ->
Reply = do_remove(Table, Object, TableDefs),
{reply, Reply, State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast({drop, Table}, #{table_defs := TableDefs, server := Server} = State) ->
tivan:drop(Table),
TableDefsU = maps:remove(Table, TableDefs),
persistent_term:put({Server, table_defs}, TableDefsU),
{noreply, State#{table_defs => TableDefsU}};
handle_cast(initialize, #{callback := Callback
,server := Server
,init_args := Arguments} = State) ->
case init([Callback, Server|Arguments]) of
{ok, NewState} ->
{noreply, NewState};
{stop, Reason} ->
{stop, Reason, State}
end;
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(Info, #{callback := Callback} = State) ->
lager:info("Info received ~p", [Info]),
case erlang:function_exported(Callback, handle_info, 1) of
true -> Callback:handle_info(Info);
false -> ok
end,
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
%%--------------------------------------------------------------------
%% #{Table => #{columns => #{Column => #{type => binary | list | tuple | atom | integer | float
%% | second | millisecond | microsecond | nanosecond
%% | OtherTable | {OtherTable, Field} | [OtherTable]
%% | [{OtherTable, Field}] | uuid | pid
%% | {any, [Type1, Type2]}
%% ,limit => undefined | Length | {Min, Max} | [Item1, Item2]
%% ,key => false | true
%% ,index => false | true
%% ,unique => false | true
%% ,null => true | false
%% ,default => undefined | Value }}
%% ,memory => true | false
%% ,persist => true | false
%% ,type => set |ordered_set | bag
%% ,audit => false | true
%% ,tags => undefined | TagStoreName
%% ,unique_combo => [] | [{column1, column2}, {... },... ]
%% ,read_context => SYSCONFIG | async_dirty | transaction | sync_transaction | etc
%% ,write_context => SYSCONFIG | async_dirty | transaction | sync_transaction | etc
%% ,mnesia_options => [] | OtherMnesiaOptions}}
%% ** The first option is the default **
%%--------------------------------------------------------------------
init_tables(TableDefs) ->
maps:map(
fun(Table, TableDef) ->
TableDefU = process_tabledef(TableDef),
init_table(Table, TableDefU),
TableDefU
end,
TableDefs
).
process_tabledef(#{columns := ColumnsNoKey} = TableDef) ->
{Key, ColumnsNoAudit} = case get_key(ColumnsNoKey) of
undefined -> {uuid, ColumnsNoKey#{uuid => #{type => uuid, key => true}}};
{K, _KeyDef} -> {K, ColumnsNoKey}
end,
Columns = case maps:get(audit, TableDef, false) of
false ->
ColumnsNoAudit;
true ->
ColumnsNoAudit#{a_ctime => #{type => integer}
,a_mtime => #{type => integer}}
end,
TableDef#{columns => Columns, key => Key}.
init_table(Table, #{columns := ColumnsWithDef} = TableDef) ->
{Key, _} = get_key(ColumnsWithDef),
ColumnsRest = maps:fold(
fun(Column, #{index := true}, ColumnsAcc) -> [{Column}|ColumnsAcc];
(Column, #{unique := true}, ColumnsAcc) -> [{Column}|ColumnsAcc];
(Column, _, ColumnsAcc) -> [Column|ColumnsAcc]
end,
[],
maps:remove(Key, ColumnsWithDef)
),
Columns = [Key|lists:sort(ColumnsRest)],
Defaults = maps:fold(
fun(Column, ColumnDef, DefaultsAcc) ->
case maps:find(default, ColumnDef) of
error -> DefaultsAcc;
{ok, Value} -> DefaultsAcc#{Column => Value}
end
end,
#{},
ColumnsWithDef
),
case maps:get(tags, TableDef, undefined) of
undefined ->
lager:info("No tags assoication needed");
TagName ->
lager:info("Creating Tags association ~p", [TagName]),
tivan_tags:create(TagName)
end,
lager:info("Initiaing creation of ~p", [{TableDef, Columns, Defaults}]),
tivan:create(Table, TableDef#{columns => Columns, defaults => Defaults}).
get_key(Columns) when is_map(Columns) -> get_key(maps:to_list(Columns));
get_key([{Column, #{key := true} = ColumnDef}|_Columns]) -> {Column, ColumnDef};
get_key([_Column|Columns]) -> get_key(Columns);
get_key([]) -> undefined.
do_put(Table, Object, TableDefs) ->
case maps:find(Table, TableDefs) of
error ->
{error, no_definition};
{ok, #{key := Key} = TableDef} ->
ObjectWithKey = update_key_curr_object(Object, Table, TableDef),
ObjectWithAudit = update_audit(ObjectWithKey, TableDef),
case validate(ObjectWithAudit, Table, TableDef) of
{ok, ObjectValidated} ->
Options = case maps:find(write_context, TableDef) of
error ->
#{};
{ok, Context} ->
#{context => Context}
end,
KeyValue = tivan:put(Table, ObjectValidated, Options),
case maps:get(tags, TableDef, undefined) of
undefined ->
ok;
TagStoreName ->
Tags = maps:get(tags, Object, undefined),
tag(TagStoreName, KeyValue, Tags)
end,
#{Key => KeyValue};
Error ->
Error
end
end.
update_key_curr_object(Object, Table, #{columns := Columns, key := Key} = TableDef) ->
KeyDef = maps:get(Key, Columns),
KeyType = maps:get(type, KeyDef, binary),
TableType = maps:get(type, TableDef, set),
case maps:find(Key, Object) of
error when KeyType == uuid ->
Value = list_to_binary(uuid:uuid_to_string(uuid:get_v4())),
Object#{Key => Value};
{ok, undefined} when KeyType == uuid ->
Value = list_to_binary(uuid:uuid_to_string(uuid:get_v4())),
Object#{Key => Value};
{ok, Value} when TableType /= bag ->
case tivan:get(Table, Value) of
[] ->
Object;
[ObjectPrev] ->
maps:merge(ObjectPrev, Object)
end;
_ ->
Object
end.
update_audit(Object, #{audit := true}) ->
Now = erlang:system_time(millisecond),
Object#{a_ctime => maps:get(a_ctime, Object, Now)
,a_mtime => Now};
update_audit(Object, _TableDef) -> Object.
validate(Object, Table, #{columns := Columns, key := Key} = TableDef) ->
case validate(Object, Table, Key, maps:iterator(Columns)) of
{ok, ObjectU} ->
UniqueComboList = maps:get(unique_combo, TableDef, []),
case validate_unique_combo(Object, Table, Key, UniqueComboList) of
ok -> {ok, ObjectU};
error -> {error, already_exists}
end;
Error ->
Error
end.
validate(Object, Table, Key, ColumnsIter) ->
case maps:next(ColumnsIter) of
{Column, ColumnDef, ColumnsIterU} ->
case get_column_value(Object, Column, ColumnDef) of
error ->
{error, {Column, not_found}};
{ok, Value} ->
KeyValue = maps:get(Key, Object, undefined),
case validate_unique(Value, Column, ColumnDef, Table, Key, KeyValue) of
error ->
{error, {Column, already_exists}};
ok ->
case validate_value(Value, ColumnDef, Table, Key, KeyValue) of
ok ->
validate(Object#{Column => Value}, Table, Key, ColumnsIterU);
Error ->
{error, {Column, Error}}
end
end
end;
none ->
{ok, Object}
end.
get_column_value(Object, Column, ColumnDef) ->
Value = maps:get(Column, Object, undefined),
get_column_value(Value, ColumnDef).
get_column_value(undefined, #{null := false}) -> error;
get_column_value(undefined, #{unique := true}) -> error;
get_column_value(undefined, #{key := true}) -> error;
get_column_value(undefined, #{default := Value}) -> {ok, Value};
get_column_value(Value, _) -> {ok, Value}.
validate_unique(Value, Column, #{unique := true}, Table, Key, KeyValue) ->
case tivan:get(Table, #{match => #{Column => Value, Key => {eval, '/=', KeyValue}}}) of
[] -> ok;
_ -> error
end;
validate_unique(_Value, _Column, _ColumnDef, _Table, _Key, _KeyValue) -> ok.
validate_value(undefined, _ColumnDef, _Table, _Key, _KeyValue) -> ok;
validate_value(Value, ColumnDef, Table, Key, KeyValue) ->
Type = maps:get(type, ColumnDef, binary),
case validate_type(Value, Type, Table, Key, KeyValue) of
true ->
Limit = maps:get(limit, ColumnDef, undefined),
case validate_limit(Value, Type, Limit) of
true ->
ok;
false ->
limit_failed
end;
false ->
type_failed
end.
validate_type(Value, uuid, _Table, _Key, _KeyValue) ->
uuid:is_v4(uuid:string_to_uuid(binary_to_list(Value)));
validate_type(Value, binary, _Table, _Key, _KeyValue) ->
is_binary(Value);
validate_type(Value, list, _Table, _Key, _KeyValue) ->
is_list(Value);
validate_type(Value, tuple, _Table, _Key, _KeyValue) ->
is_tuple(Value);
validate_type(Value, boolean, _Table, _Key, _KeyValue) ->
is_boolean(Value);
validate_type(Value, atom, _Table, _Key, _KeyValue) ->
is_atom(Value);
validate_type(Value, integer, _Table, _Key, _KeyValue) ->
is_integer(Value);
validate_type(Value, float, _Table, _Key, _KeyValue) ->
is_float(Value);
validate_type(Value, map, _Table, _Key, _KeyValue) ->
is_map(Value);
validate_type(Value, pid, _Table, _Key, _KeyValue) ->
is_pid(Value) andalso is_process_alive(Value);
validate_type(Value, second, _Table, _Key, _KeyValue) when is_integer(Value) ->
(Value =< erlang:system_time(second)) and (Value > 0);
validate_type(Value, millisecond, _Table, _Key, _KeyValue) when is_integer(Value) ->
(Value =< erlang:system_time(millisecond)) and (Value > 0);
validate_type(Value, microsecond, _Table, _Key, _KeyValue) when is_integer(Value) ->
(Value =< erlang:system_time(microsecond)) and (Value > 0);
validate_type(Value, nanosecond, _Table, _Key, _KeyValue) when is_integer(Value) ->
(Value =< erlang:system_time(nanosecond)) and (Value > 0);
validate_type(Value, {any, Types}, _Table, _Key, _KeyValue) when is_list(Types) ->
lists:any(
fun(Type) -> validate_type(Value, Type, _Table, _Key, _KeyValue) end,
Types
);
validate_type(Values, [{Table, Field}], _Table, _Key, _KeyValue) when is_list(Values) ->
lists:all(
fun(Value) ->
validate_type(Value, {Table, Field}, _Table, _Key, _KeyValue)
end,
Values
);
validate_type(Value, {Table, Field}, _Table, _Key, _KeyValue) when is_atom(Table), is_atom(Field) ->
case catch tivan:get(Table, #{match => #{Field => Value}}) of
{'EXIT', _Reason} -> false;
[] -> false;
_ -> true
end;
validate_type(Values, [Table], _Table, _Key, _KeyValue) when is_list(Values) ->
lists:all(
fun(Value) ->
validate_type(Value, Table, _Table, _Key, _KeyValue)
end,
Values
);
validate_type(Value, Table, Table, _Key, Value) -> false;
validate_type(Value, Table, _Table, _Key, _KeyValue) when is_atom(Table) ->
case catch tivan:get(Table, Value) of
{'EXIT', _Reason} -> false;
[] -> false;
_ -> true
end;
validate_type(_Value, _Type, _Table, _Key, _KeyValue) -> false.
validate_limit(_Value, _Type, undefined) -> true;
validate_limit(Value, binary, Size) when is_integer(Size) -> size(Value) =< Size;
validate_limit(Value, tuple, Size) when is_integer(Size) -> size(Value) =< Size;
validate_limit(Value, map, Size) when is_integer(Size) -> map_size(Value) =< Size;
validate_limit(Value, list, Size) when is_integer(Size) -> length(Value) =< Size;
validate_limit(Value, integer, Size) when is_integer(Size) -> Value =< Size;
validate_limit(Value, binary, {re, RegExp}) when is_binary(Value) ->
case re:run(Value, RegExp) of
nomatch -> false;
{match, _} -> true
end;
validate_limit(Value, binary, {Min, Max}) when is_integer(Min),is_integer(Max) ->
Size = size(Value),
(Size >= Min) and (Size =< Max);
validate_limit(Value, tuple, {Min, Max}) when is_integer(Min),is_integer(Max) ->
Size = size(Value),
(Size >= Min) and (Size =< Max);
validate_limit(Value, map, {Min, Max}) when is_integer(Min),is_integer(Max) ->
Size = map_size(Value),
(Size >= Min) and (Size =< Max);
validate_limit(Value, list, {Min, Max}) when is_integer(Min),is_integer(Max) ->
Size = length(Value),
(Size >= Min) and (Size =< Max);
validate_limit(Value, integer, {Min, Max}) when is_integer(Min),is_integer(Max) ->
(Value >= Min) and (Value =< Max);
validate_limit(Value, _Type, List) when is_list(List) -> lists:member(Value, List);
validate_limit(_Value, _Type, _Limit) -> true.
validate_unique_combo(Object, Table, Key, [ComboTuple|UniqueComboList]) ->
KeyValue = maps:get(Key, Object, undefined),
ColumnValueMap = lists:foldl(
fun(Column, CVMap) ->
CVMap#{Column => maps:get(Column, Object, undefined)}
end,
#{},
tuple_to_list(ComboTuple)
),
case tivan:get(Table, #{match => ColumnValueMap#{Key => {eval, '/=', KeyValue}}}) of
[] ->
validate_unique_combo(Object, Table, Key, UniqueComboList);
_ ->
error
end;
validate_unique_combo(_Object, _Table, _Key, []) -> ok.
tag(TagStoreName, KeyValue, undefined) ->
tag(TagStoreName, KeyValue, []);
tag(TagStoreName, KeyValue, TagsAfter) ->
TagsBefore = tivan_tags:tags(TagStoreName, KeyValue),
[ tivan_tags:untag(TagStoreName, KeyValue, Tag) || Tag <- TagsBefore -- TagsAfter ],
[ tivan_tags:tag(TagStoreName, KeyValue, Tag) || Tag <- TagsAfter -- TagsBefore ].
do_get(Table, Options, TableDefs) ->
case maps:find(Table, TableDefs) of
error ->
{error, no_definition};
{ok, TableDef} ->
do_get_1(Table, Options, TableDef, TableDefs)
end.
do_get_1(Table, Options, #{columns := ColumnsMap} = TableDef, TableDefs)
when is_map(Options) ->
ColumnsToMatch = ['_'|maps:keys(ColumnsMap)],
OptionsFormatted = interpret_get_options(Options, ColumnsToMatch),
OptionsForTags = options_for_tags(OptionsFormatted, TableDef),
OptionsWithContext = case maps:find(read_context, TableDef) of
error ->
OptionsForTags;
{ok, Context} ->
OptionsForTags#{context => Context}
end,
Objects = tivan:get(Table, OptionsWithContext),
ObjectsWithTags = objects_with_tags(OptionsWithContext, Table, TableDef, Objects),
ObjectsCleanedUp = remove_key_if_not_asked(OptionsFormatted, TableDef, ObjectsWithTags),
ObjectsExpanded = [ expand(Object, OptionsFormatted, TableDef, TableDefs)
|| Object <- ObjectsCleanedUp ],
ObjectsFlattend = flatten(ObjectsExpanded, OptionsFormatted),
paginate(ObjectsFlattend, OptionsFormatted, Table);
do_get_1(Table, KeyValue, #{key := Key} = TableDef, TableDefs) ->
do_get_1(Table, #{Key => KeyValue}, TableDef, TableDefs).
interpret_get_options(#{match := _} = Options, _Columns) ->
Options;
interpret_get_options(Options, Columns) ->
Match = maps:filter(
fun(Option, _Value) ->
lists:member(Option, Columns)
end,
Options
),
lists:foldl(
fun({Alternate, Original}, OptionsAcc) ->
case maps:find(Original, Options) of
error ->
case maps:find(Alternate, Options) of
error -> OptionsAcc;
{ok, Value} -> OptionsAcc#{Original => Value}
end;
{ok, Value} -> OptionsAcc#{Original => Value}
end
end,
#{match => Match},
[{'_select',select}
,{'_expand', expand}
,{'_start', start}
,{'_limit', limit}
,{'_sort_column', sort_column}
,{'_sort_order', sort_order}
,{'_cache', cache}
,{'_flatten', flatten}]
).
options_for_tags(Options, #{key := Key} = TableDef) ->
TagsDef = maps:get(tags, TableDef, undefined),
Selects = maps:get(select, Options, []),
Match = maps:get(match, Options, #{}),
IsTagSelected = Selects == [] orelse lists:member(tags, Selects),
IsKeySelected = Selects == [] orelse lists:member(Key, Selects),
IsTagForMatch = maps:is_key(tags, Match) orelse maps:is_key('_', Match),
if
TagsDef =/= undefined
, (IsTagSelected or IsTagForMatch)
, not IsKeySelected ->
Options#{select => [Key|Selects]};
true ->
Options
end.
objects_with_tags(Options, Table, #{key := Key} = TableDef, Objects) ->
TagsDef = maps:get(tags, TableDef, undefined),
Selects = maps:get(select, Options, []),
Match = maps:get(match, Options, #{}),
IsTagSelected = Selects == [] orelse lists:member(tags, Selects),
IsTagForMatch = maps:is_key(tags, Match) orelse maps:is_key('_', Match),
lager:info("TagsDef ~p, Select ~p, Match ~p, IsTagSelected ~p, IsTagForMatch ~p"
, [TagsDef, Selects, Match, IsTagSelected, IsTagForMatch]),
case TagsDef of
undefined ->
Objects;
TagStoreName ->
TagsToSearch = maps:get(tags, Match, []) ++ maps:get('_', Match, []),
Entities = tivan_tags:entities(TagStoreName, TagsToSearch),
ObjectsU = if Objects == [], Entities =/= undefined ->
tivan:get(Table, Options#{match => maps:remove('_', Match)});
true -> Objects end,
lists:filtermap(
fun(Object) when IsTagForMatch, Entities =/= undefined ->
#{Key := KeyValue} = Object,
case lists:member(KeyValue, Entities) of
false -> false;
true when IsTagSelected ->
Tags = tivan_tags:tags(TagStoreName, KeyValue),
{true, Object#{tags => Tags}};
true -> true
end;
(Object) when IsTagSelected ->
#{Key := KeyValue} = Object,
Tags = tivan_tags:tags(TagStoreName, KeyValue),
{true, Object#{tags => Tags}};
(Object) -> Object
end,
ObjectsU
)
end.
remove_key_if_not_asked(Options, #{key := Key} = TableDef, Objects) ->
TagsDef = maps:get(tags, TableDef, undefined),
Selects = maps:get(select, Options, []),
Match = maps:get(match, Options, #{}),
IsTagSelected = Selects == [] orelse lists:member(tags, Selects),
IsKeySelected = Selects == [] orelse lists:member(Key, Selects),
IsTagForMatch = maps:is_key(tags, Match) orelse maps:is_key('_', Match),
if
TagsDef =/= undefined
, (IsTagSelected or IsTagForMatch)
, not IsKeySelected ->
[ maps:remove(Key, O) || O <- Objects ];
true ->
Objects
end.
expand(Object, #{expand := 0}, _TableDef, _TableDefs) -> Object;
expand(Object, #{expand := Level}, TableDef, TableDefs) when is_integer(Level) andalso Level > 0 ->
maps:map(
fun(Column, Value) ->
case TableDef of
#{columns := #{Column := #{type := ColumnType}}} ->
ValueExpanded = get_referred_object(ColumnType, Value),
case maps:find(ColumnType, TableDefs) of
error -> ValueExpanded;
{ok, TableDefOfValue} ->
expand(ValueExpanded, #{expand => Level - 1}, TableDefOfValue, TableDefs)
end;
_ ->
Value
end
end,
Object
);
expand(Object, _Options, _TableDef, _TableDefs) -> Object.
get_referred_object(_ColumnType, undefined) -> undefined;
get_referred_object(ColumnType, Value) ->
IsNative = lists:member(ColumnType, ?NATIVE_TYPES),
case ColumnType of
_NativeType when IsNative -> Value;
{any, ColumnTypes} ->
try
lists:foldl(
fun(Type, ValueA) ->
case get_referred_object(Type, ValueA) of
Value -> Value;
ValueExpanded -> throw(ValueExpanded)
end
end,
Value,
ColumnTypes
)
catch
throw:ValueExpanded -> ValueExpanded
end;
[{Table, Field}] when is_list(Value) ->
lists:flatten([ get_referred_object({Table, Field}, X) || X <- Value ]);
[Table] when is_list(Value) ->
lists:flatten([ get_referred_object(Table, X) || X <- Value ]);
{Table, Field} ->
case tivan:get(Table, #{match => #{Field => Value}}) of
[] -> Value;
[ValueExpanded] -> ValueExpanded#{'_type' => Table};
ValuesExpanded -> [ X#{'_type' => Table} || X <- ValuesExpanded ]
end;
Table ->
case tivan:get(Table, Value) of
[] -> Value;
[ValueExpanded] -> ValueExpanded#{'_type' => Table};
ValuesExpanded -> [ X#{'_type' => Table} || X <- ValuesExpanded ]
end
end.
flatten(Objects, #{flatten := True}) when True == true; True == <<"true">> ->
[ flatten(Object) || Object <- Objects ];
flatten(Objects, _OptionsU) -> Objects.
flatten(Object) ->
case maps:to_list(Object) of
[{_Key, Value}] -> Value;
_ -> Object
end.
paginate(Objects, #{limit := Limit} = Options, Table) ->
Start = maps:get(start, Options, 1),
Cache = case maps:find(cache, Options) of
error ->
initialize_cache(Objects);
{ok, Id} when is_reference(Id) ->
case tivan_page:info(Id) of
undefined ->
initialize_cache(Objects);
_ ->
Id
end
end,
case maps:find(sort_column, Options) of
error -> ok;
{ok, SortColumn} ->
SortOrder = case maps:get(sort_order, Options, asc) of
desc -> desc;
_ -> asc
end,
tivan_page:sort(Cache, {SortColumn, SortOrder})
end,
ObjectsLimited = tivan_page:get(Cache, #{start => Start, limit => Limit}),
#{size := Size} = tivan_page:info(Cache),
#{Table => ObjectsLimited, cache => Cache, size => Size};
paginate(Objects, #{sort_column := SortColumn} = Options, _Table) ->
SortFun = case maps:get(sort_order, Options, asc) of
desc ->
fun(#{SortColumn := ValueA}, #{SortColumn := ValueB}) ->
ValueA > ValueB;
(_A, _B) -> true
end;
_ ->
fun(#{SortColumn := ValueA}, #{SortColumn := ValueB}) ->
ValueA < ValueB;
(_A, _B) -> true
end
end,
lists:sort(SortFun, Objects);
paginate(Objects, _Options, _Table) -> Objects.
initialize_cache(Objects) ->
Id = tivan_page:new(),
ok = tivan_page:put(Id, Objects),
Id.
do_remove(Table, Object, TableDefs) ->
case maps:find(Table, TableDefs) of
error ->
{error, no_definition};
{ok, TableDef} ->
do_remove_1(Table, Object, TableDef)
end.
do_remove_1(Table, Object, #{key := Key} = TableDef) when is_map(Object) ->
case maps:find(Key, Object) of
error ->
{error, no_key};
{ok, KeyValue} ->
Options = case maps:find(write_context, TableDef) of
error ->
#{};
{ok, Context} ->
#{context => Context}
end,
case maps:get(tags, TableDef, undefined) of
undefined ->
ok;
TagStoreName ->
Tags = tivan_tags:tags(TagStoreName, KeyValue),
[ tivan_tags:untag(TagStoreName, KeyValue, Tag) || Tag <- Tags ]
end,
tivan:remove(Table, KeyValue, Options)
end;
do_remove_1(Table, KeyValue, #{key := Key} = TableDef) ->
do_remove_1(Table, #{Key => KeyValue}, TableDef).