diff --git a/.gitignore b/.gitignore index b51f948..0eb82ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ .DS_Store /_build /deps +/doc/* /priv/static/ -/log/**/*.log +/https://community.openai.com/t/any-updates-on-assistant-api-streaming/551809/56log/**/*.log .tool-versions .idea diff --git a/README.md b/README.md index cfbb752..ceca3dc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,19 @@ The Adapter supports 1 main Repo, but also dynamic repos where you can querie on multiple Dbs with a dynamic supervisor. +To create a Repo follow the Ecto default instructions, but we will need to add returning true option in the Repo module, +this is in order to have the _rev value updated into the documents. + +``` +defmodule MyApp.Repo do + use Ecto.Repo, + otp_app: :my_app, + adapter: Couchx.Adapter + + def default_options(_), do: [returning: true] +end +``` + The supported functions in this version are: ``` @@ -107,7 +120,7 @@ It rely on repos declared under `config.exs` ``` import Config -config :my_app, ecto_repos: ["repo", "custom_repo"] +config :my_app, ecto_repos: [MyApp.Repo] ... ``` @@ -119,7 +132,7 @@ defmodule MyApp.Repo.Index.MyMangoIndex do def up do create_index "my-mango-index" do - %{fields: ["name", "email"]} + %{fields: ~w[name email]} end end @@ -139,6 +152,24 @@ And if you want to remove the index you can call: `$ mix couch.mango_index.down -r MyApp.Repo, -n my-mango-index` +Mango indexes could be run in the release like this: + +```elixir +defmodule MyApp.Release do + @moduledoc """ + Used for executing DB release tasks when run in production without Mix + installed. + """ + + @app :my_app + + def migrate do + Application.ensure_all_started(@app) + Couchx.Migrator.run(MyApp.Repo, :up) + end +end +``` + ## TODO: * Repo.insert_all diff --git a/lib/couchx/adapter.ex b/lib/couchx/adapter.ex index 60f4c3a..ee9b988 100644 --- a/lib/couchx/adapter.ex +++ b/lib/couchx/adapter.ex @@ -25,12 +25,11 @@ defmodule Couchx.Adapter do Couchx supports 1 main repo and many dynamic supervised repos. A dynamic repo will allow you to have multiple db connections in your application. - To achieve this, you will need to setup a `DynamicSupervisor` and a `Registry` in the application like: + To achieve this, you will need to setup a `Registry` in the application like: ``` def start(_type, _args) do children = [ - {DynamicSupervisor, strategy: :one_for_one, name: CouchxSupervisor} {Registry, keys: :unique, name: CouchxRegistry}, ... ] @@ -128,7 +127,6 @@ defmodule Couchx.Adapter do ``` Removing the documents from the database. - """ import Couchx.CouchId @@ -136,6 +134,8 @@ defmodule Couchx.Adapter do @behaviour Ecto.Adapter.Schema @behaviour Ecto.Adapter.Queryable + @encodable_keys ~w[key keys startkey endkey start_key end_key]a + defmacro __before_compile__(_env), do: :ok def init(config) do @@ -177,7 +177,7 @@ defmodule Couchx.Adapter do constraints = Constraint.call(meta[:pid], repo, fields) constraints - |> DocumentState.merge_constraints + |> DocumentState.merge_constraints() |> do_insert(repo, constraints, fields, returning, meta) end @@ -202,26 +202,32 @@ defmodule Couchx.Adapter do end def execute(:view, meta, design, view, key, query_opts) do - opts = query_opts - |> Enum.into(%{}) - |> Map.merge(%{key: key}) + query_opts = query_opts ++ [key: Jason.encode!(key)] + execute(:view, meta, design, view, query_opts) + end + def execute(:view, meta, design, view, query_opts) do + opts = prepare_view_options(query_opts) Couchx.DbConnection.get(meta[:pid], "_design/#{design}/_view/#{view}", opts) - |> parse_view_response(opts[:include_docs]) + |> parse_view_response(opts[:include_docs], query_opts[:module]) end def execute(:find, meta, selector, fields, opts) do query = %{selector: selector, fields: fields} - Couchx.DbConnection.find(meta[:pid], query, opts) - |> parse_view_response(opts[:include_docs]) + |> parse_view_response(opts[:include_docs], opts[:module]) + end + + def execute(:request, meta, method, path, opts) do + Couchx.DbConnection.raw_request(meta[:pid], method, path, opts) + |> parse_view_response(opts[:include_docs], opts[:module]) end def execute(meta, query_meta, query_cache, params, _opts) do {_, {_, query}} = query_cache %{select: select} = query_meta - keys = query[:keys] - query_options = query[:options] + keys = fetch_query_keys(query_cache) + query_options = query[:options] || %{} {all_fields, module} = fetch_fields(query_meta.sources) namespace = build_namespace(module) @@ -244,6 +250,11 @@ defmodule Couchx.Adapter do |> QueryHandler.query_results(fields, fields_meta) end + defp fetch_query_keys({_, {_, query}}) + when query == [:delete], do: [:delete] + + defp fetch_query_keys({_, {_, query}}), do: query[:keys] + def create_admin(server, name, password) do Couchx.DbConnection.create_admin(server, name, password) end @@ -358,23 +369,61 @@ defmodule Couchx.Adapter do end defp do_query(server, properties, namespace, values, query_options) when is_list(properties) do - selector = Enum.reduce(properties, %{type: namespace}, &process_property(&1, &2, values)) + selector = extract_properties(namespace, properties, values) + query_options = extract_options_properties(namespace, query_options, values) query = select_query(selector, query_options) Couchx.DbConnection.find(server, query) end + defp extract_options_properties(namespace, query_options, values) do + extract_properties(namespace, query_options, values) + |> Map.drop([:type]) + end + + defp extract_properties(namespace, [%{"$and" => properties}], values) do + extract_properties(namespace, properties, values) + end + + defp extract_properties(namespace, properties, values) do + Enum.reduce(properties, %{type: namespace}, &process_property(&1, &2, values)) + end + defp select_query(selector, options) do %{selector: selector} |> Map.merge(options) end + defp process_property({key, selector}, acc, values) + when is_map(selector) do + with [operator] <- Map.keys(selector), + false <- operator == "$eq" do + %{key => process_selector(selector, values)} + else + true -> + case selector do + %{"$eq" => {_, [], [value_index]}} -> + value = Enum.fetch!(values, value_index) + Map.put(acc, key, value) + %{"$eq" => value} -> + Map.put(acc, key, value) + _ -> + {:error, "unsupported selector"} + end + end + end + defp process_property({key, {:^, [], [value_index]}}, acc, values) do value = Enum.fetch!(values, value_index) Map.put(acc, key, value) end defp process_property({key, value}, acc, _values) do - Map.put(acc, key, value) + case key do + "$or" -> + Map.put(acc, "$and", [%{key => value}]) + _ -> + Map.put(acc, key, value) + end end defp process_property(property, acc, values) do @@ -382,7 +431,11 @@ defmodule Couchx.Adapter do |> Map.merge(acc) end - #defp do_query(_, _, _, _), do: {:error, :not_implemented} + defp process_selector(%{"$in" => {:^, [], [start, amount]}}, values) do + %{"$in" => Enum.slice(values, start, amount)} + end + + defp process_selector(selector, _values), do: selector defp build_namespace(module) do module @@ -420,25 +473,35 @@ defmodule Couchx.Adapter do Map.put(data, :_id, id) end - defp parse_view_response({:ok, %{"rows" => rows}}, true) do + defp parse_view_response({:ok, %{"rows" => rows}}, true, module_name) do rows |> Enum.map(&Map.get(&1, "doc")) - |> Enum.map(&build_structs/1) + |> Enum.filter(& &1) + |> Enum.map(&build_structs(&1, module_name)) + end + + defp parse_view_response({:ok, %{"rows" => rows}}, _, _), do: rows + defp parse_view_response({:ok, %{"bookmark" => _, "docs" => docs}}, _, _), do: docs + defp parse_view_response({:ok, raw_response}, _, _), do: raw_response + + defp parse_view_response({:error, _} = error, _, _), do: error + + defp build_structs(map, module_name) do + doc = Enum.reduce(map, %{}, &keys_to_atoms/2) + module_name = fetch_module_name(map, module_name) + struct(module_name, doc) end - defp parse_view_response({:ok, %{"rows" => rows}}, _), do: rows - defp parse_view_response({:ok, %{"bookmark" => _, "docs" => docs}}, _), do: docs - defp build_structs(map) do + defp fetch_module_name(map, nil) do doc_type = Map.get(map, "_id") |> String.replace(~r{(/.+)}, "") |> Macro.camelize - module = :"Elixir.SDB.#{doc_type}" # TODO: pass module name in view execute - - doc = Enum.reduce(map, %{}, &keys_to_atoms/2) - struct(module, doc) + :"Elixir.SDB.#{doc_type}" end + defp fetch_module_name(_map, name), do: name + defp keys_to_atoms({key, value}, acc) do Map.put(acc, String.to_atom(key), value) end @@ -446,20 +509,20 @@ defmodule Couchx.Adapter do defp put_conn_id(config), do: config ++ [id: config[:name]] defp couchdb_supervisor_spec(config) do - { - config[:id], - { - DynamicSupervisor, - :start_child, + sup_id = config[:id] || CouchxAdapter + + %{ + id: sup_id, + start: { + Couchx.DbConnection, + :start_link, [ - CouchxSupervisor, - {Couchx.DbConnection, config} + config, ] }, - :permanent, - :infinity, - :worker, - [config[:id]] + restart: :permanent, + shutdown: :infinity, + type: :supervisor } end @@ -468,8 +531,12 @@ defmodule Couchx.Adapter do # Pending implementation - def delete(meta, _meta_schema, params, _opts) do - doc_id = URI.encode_www_form(params[:_id]) + def delete(meta, meta_schema, params, _opts) do + doc_id = meta_schema + |> Map.get(:schema) + |> build_namespace() + |> namespace_id(params[:_id]) + Couchx.DbConnection.get(meta[:pid], doc_id) |> find_to_delete(meta[:pid], doc_id) end @@ -530,10 +597,14 @@ defmodule Couchx.Adapter do end defp try_to_persist_insert(%{ok: _}, data, returning, meta, url, body) do - {:ok, response} = Couchx.DbConnection.insert(meta[:pid], url, body) - response = Map.merge(data, %{_id: response["id"], _rev: response["rev"]}) - values = Enum.map(returning, fn(k)-> Map.get(response, k) end) - {:ok, Enum.zip(returning, values)} + case Couchx.DbConnection.insert(meta[:pid], url, body) do + {:ok, response} -> + values = Map.merge(data, %{_rev: response["rev"]}) + values = fetch_insert_values(response, values, returning) + {:ok, Enum.zip(returning, values)} + {:error, error} -> + {:error, error} + end end defp do_update(errors, _constraints, _id, _response, _data, _returning, _server) @@ -573,7 +644,10 @@ defmodule Couchx.Adapter do end defp fetch_insert_values(%{"ok" => true}, response, returning) do - data = for {key, val} <- response, into: %{}, do: {String.to_atom(key), val} + data = case response do + %{_id: _id} -> response + _ -> for {key, val} <- response, into: %{}, do: {String.to_atom(key), val} + end Enum.map(returning, fn(k)-> Map.get(data, k) @@ -615,4 +689,12 @@ defmodule Couchx.Adapter do |> namespace_id(id) |> URI.decode_www_form end + + defp prepare_view_options(options) do + @encodable_keys + |> Enum.reduce(options, fn key, acc -> + Keyword.replace(acc, key, Jason.encode!(options[key])) + end) + |> Enum.into(%{}) + end end diff --git a/lib/couchx/constraint.ex b/lib/couchx/constraint.ex index b19e61b..7ceaabd 100644 --- a/lib/couchx/constraint.ex +++ b/lib/couchx/constraint.ex @@ -9,7 +9,8 @@ defmodule Couchx.Constraint do ) do if with_schema?(schema) do params = Enum.into(fields, %{}) - changeset = schema.changeset(schema.__struct__, params) + schema_struct = struct(schema.__struct__, prev_fields) + changeset = schema.changeset(schema_struct, params) fields = Keyword.merge(prev_fields, fields) changeset diff --git a/lib/couchx/db_connection.ex b/lib/couchx/db_connection.ex index 2959c4c..ec62e8b 100644 --- a/lib/couchx/db_connection.ex +++ b/lib/couchx/db_connection.ex @@ -1,5 +1,5 @@ defmodule Couchx.DbConnection do - use GenServer, restart: :temporary + use GenServer, restart: :transient def start_link(args) do config = build_config(args) @@ -12,6 +12,10 @@ defmodule Couchx.DbConnection do {:ok, args} end + def terminate(reason, _state) do + IO.inspect reason + end + def info(server), do: GenServer.call(server, :info) def insert(server, resource, body, options \\ []) do @@ -63,6 +67,10 @@ defmodule Couchx.DbConnection do GenServer.call(server, {:index, doc}) end + def raw_request(server, method, path, options \\ []) do + GenServer.call(server, {:raw_request, method, path, options}) + end + def handle_call({:index, doc}, _from, state) do headers = state[:base_headers] url = "#{state[:base_url]}/_index" @@ -156,6 +164,23 @@ defmodule Couchx.DbConnection do |> call_response(state) end + def handle_call({:raw_request, method, path, options}, _from, state) do + query_str = build_query_str(options[:query_str]) + url = "#{state[:base_url]}/#{path}#{query_str}" + + case method do + :get -> + request(method, url, [headers: state[:base_headers], options: state[:options]]) + :delete -> + request(:delete, url, [headers: state[:base_headers], options: []]) + _ -> + body = Jason.encode!(options[:body]) + request(method, url, body, [headers: state[:base_headers], options: state[:options]]) + end + |> call_response(state) + + end + def handle_call({:find, query, options}, _from, state) do headers = state[:base_headers] query_str = build_query_str(options[:query_str]) diff --git a/lib/couchx/dynamic_repo.ex b/lib/couchx/dynamic_repo.ex index fc9d7af..c93a203 100644 --- a/lib/couchx/dynamic_repo.ex +++ b/lib/couchx/dynamic_repo.ex @@ -17,20 +17,24 @@ defmodule Couchx.DynamicRepo do name = if (is_atom(name)), do: name, else: String.to_atom(name) default_dynamic_repo = get_dynamic_repo() start_opts = [name: name] ++ credentials - {:ok, repo} = __MODULE__.start_link(start_opts) + repo = __MODULE__.start_link(start_opts) + |> maybe_fetch_repo() try do __MODULE__.put_dynamic_repo(repo) callback.() after __MODULE__.put_dynamic_repo(default_dynamic_repo) - DynamicSupervisor.stop(repo) + if Process.alive?(repo), do: Supervisor.stop(repo) end end defp fetch_config do Application.get_env(@otp_app, __MODULE__) end + + defp maybe_fetch_repo({:ok, repo}), do: repo + defp maybe_fetch_repo({:error, {:already_started, repo}}), do: repo end end end diff --git a/lib/couchx/mango_index.ex b/lib/couchx/mango_index.ex index 9bf4ee6..1e5c107 100644 --- a/lib/couchx/mango_index.ex +++ b/lib/couchx/mango_index.ex @@ -28,14 +28,14 @@ defmodule Couchx.MangoIndex do defp build_index(index), do: %{index: index} defp persist_index(doc) do - {adapter, meta} = Ecto.Repo.Registry.lookup(@repo_name) - Couchx.DbConnection.index(meta[:pid], doc) + repo = Ecto.Repo.Registry.lookup(@repo_name) + Couchx.DbConnection.index(repo.pid, doc) |> handle_response end defp delete_index(name, id \\ nil) do - {adapter, meta} = Ecto.Repo.Registry.lookup(@repo_name) - Couchx.DbConnection.delete(meta[:pid], :index, name, id) + repo = Ecto.Repo.Registry.lookup(@repo_name) + Couchx.DbConnection.delete(repo.pid, :index, name, id) |> handle_response end diff --git a/lib/couchx/prepare_query.ex b/lib/couchx/prepare_query.ex index 8ee7c3c..abc1741 100644 --- a/lib/couchx/prepare_query.ex +++ b/lib/couchx/prepare_query.ex @@ -6,6 +6,17 @@ defmodule Couchx.PrepareQuery do in: "$in" ] + @operators [ + ==: "$eq", + >: "$gt", + <: "$lt", + >=: "$gte", + <=: "$lte", + !=: "$ne" + ] + + @operator_keys Keyword.keys(@operators) + def call(%{wheres: wheres, limit: _limit} = query) do keys = Enum.map(wheres, &parse_where/1) options = parse_options(query) @@ -25,8 +36,17 @@ defmodule Couchx.PrepareQuery do build_query_condition(condition, fields) end - defp build_query_condition(_, [{{_, [], [{_, [], [_]}, key]}, [], []}, value]) do - %{ key => value } + defp build_query_condition(condition, [{{_, [], [{_, [], [_]}, key]}, [], []}, value]) do + cond do + condition == :== -> + %{ key => value } + condition == :in and key == :_id -> + %{ key => value } + condition in @operator_keys -> + %{ key => %{ @operators[condition] => value } } + true -> + {:error, "invalid query operator"} + end end defp build_query_condition(condition, fields) do @@ -39,14 +59,30 @@ defmodule Couchx.PrepareQuery do defp build_field_condition({:^, [], [0]}), do: :primary_key defp build_field_condition({{_, _, [{_, _, [0]}, key]}, _, _}), do: %{key => :empty} + + defp build_field_condition({expr, _, [{{_, _, [{_, _, _}, key]}, _, _}, value]}) do + %{ key => %{ @query_map[expr] => value } } + end + defp build_field_condition({expr, _, [{{_, _, [_, key]}, _, _}, value]}) do %{ key => %{ @query_map[expr] => value } } end - defp parse_options(%{order_bys: order_bys, limit: limit}) do + defp build_field_condition({expr, _, list}) + when expr in ~w[== and]a and is_list(list) do + Enum.reduce(list, %{}, &Map.merge(&2, build_field_condition(&1))) + end + + defp build_field_condition({expr, _, list}) + when expr == :or and is_list(list) do + %{"$or": [Enum.reduce(list, %{}, &Map.merge(&2, build_field_condition(&1)))] } + end + + defp parse_options(%{order_bys: order_bys, limit: limit, offset: skip}) do %{} |> try_add_limit(limit) |> try_add_order(order_bys) + |> try_add_skip(skip) end defp try_add_limit(options, nil), do: options @@ -61,6 +97,12 @@ defmodule Couchx.PrepareQuery do Map.merge(opts, %{sort: order}) end + defp try_add_skip(options, nil), do: options + defp try_add_skip(options, %{expr: skip}) do + options + |> Map.merge(%{skip: skip}) + end + defp parse_orders({order, {{_, _, [_, field]}, _, _}}) do %{"#{field}": order} end diff --git a/lib/couchx/query_handler.ex b/lib/couchx/query_handler.ex index 3c42a80..90a2aaf 100644 --- a/lib/couchx/query_handler.ex +++ b/lib/couchx/query_handler.ex @@ -6,12 +6,23 @@ defmodule Couchx.QueryHandler do ] def query_results([], _, _), do: {0, []} - def query_results({:error, reason}, _, _), do: raise Couchx.DbError, message: "#{reason}" + def query_results({:error, reason}, _, _), do: raise(Couchx.DbError, message: "#{reason}") + + def query_results([%{"_id" => _}|_] = docs, fields, metadata) do + Enum.map(docs, &process_docs(&1, fields, metadata)) + |> execute_response + end def query_results({:ok, response}, _, _) when response in @empty_response do {0, []} end + def query_results({:ok, response}, fields, metadata) + when is_list(response) do + Enum.map(response, &query_results(&1, fields, metadata)) + |> execute_response + end + def query_results({:ok, response}, fields, metadata) do query_results(response, fields, metadata) end @@ -30,6 +41,10 @@ defmodule Couchx.QueryHandler do process_docs(doc, fields, metadata) end + def query_results(%{"ok" => true, "id"=> id, "rev"=> rev}, _fields, nil) do + [_id: id, _rev: rev] + end + def query_results(doc, fields, metadata) do process_docs(doc, fields, metadata) |> execute_response diff --git a/lib/mix/tasks/couchx.gen.mango_index.ex b/lib/mix/tasks/couchx.gen.mango_index.ex index 46ad5de..51a0c1e 100644 --- a/lib/mix/tasks/couchx.gen.mango_index.ex +++ b/lib/mix/tasks/couchx.gen.mango_index.ex @@ -81,7 +81,7 @@ defmodule Mix.Tasks.Couchx.Gen.MangoIndex do defp parsed_fields([]), do: "[]" defp fields_to_sigil(fields) do - "~#{split_fields(List.first(fields))}" + "~w[#{split_fields(List.first(fields))}]" end defp split_fields(fields) do diff --git a/mix.exs b/mix.exs index 16a753a..ce75919 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Couchx.MixProject do def project do [ app: :couchx, - version: "0.2.2", + version: "0.4.7", elixir: "~> 1.12", name: "Couchx", description: "Limited CouchDb Adapter for Ecto", @@ -27,8 +27,8 @@ defmodule Couchx.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:earmark, "~> 0.1", only: :dev}, - {:ecto_sql, "~> 3.0"}, + {:earmark, "~> 1.4", only: :dev}, + {:ecto_sql, "~> 3.10"}, {:ex_doc, "~> 0.11", only: :dev}, {:httpoison, "~> 1.8"}, {:inflex, "~> 2.0.0"}, diff --git a/mix.lock b/mix.lock index 26777d0..d1135f0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,18 @@ %{ "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, - "db_connection": {:hex, :db_connection, "2.4.0", "d04b1b73795dae60cead94189f1b8a51cc9e1f911c234cc23074017c43c031e5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad416c21ad9f61b3103d254a71b63696ecadb6a917b36f563921e0de00d7d7c8"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, - "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], [], "hexpm", "c86afb8d22a5aa8315afd4257c7512011c0c9a48b0fea43af7612836b958098b"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"}, - "ecto": {:hex, :ecto, "3.7.1", "a20598862351b29f80f285b21ec5297da1181c0442687f9b8329f0445d228892", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d36e5b39fc479e654cffd4dbe1865d9716e4a9b6311faff799b6f90ab81b8638"}, - "ecto_sql": {:hex, :ecto_sql, "3.7.0", "2fcaad4ab0c8d76a5afbef078162806adbe709c04160aca58400d5cbbe8eeac6", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.4.0 or ~> 0.5.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a26135dfa1d99bf87a928c464cfa25bba6535a4fe761eefa56077a4febc60f70"}, + "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "ex_doc": {:hex, :ex_doc, "0.25.2", "4f1cae793c4d132e06674b282f1d9ea3bf409bcca027ddb2fe177c4eed6a253f", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5b0c172e87ac27f14dfd152d52a145238ec71a95efbf29849550278c58a393d6"}, "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "inflex": {:hex, :inflex, "2.0.0", "db69d542b8fdb23ac667f9bc0c2395a3983fa2da6ae2efa7ab5dc541928f7a75", [:mix], [], "hexpm", "c018852409bd48b03ad96ed53594186bc074bdd1519043a0ad1fa5697aac4399"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, @@ -21,6 +21,6 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, }