Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
132caee
allow module name on query
javierg Aug 8, 2022
0b27934
adapter raw request
javierg Aug 19, 2022
627dfe8
handle repo dynamic supervisor state
javierg Aug 29, 2022
9868814
push version
javierg Sep 9, 2022
9deb931
handle update/create
javierg Oct 30, 2023
97ef8c8
push patch version
javierg Oct 30, 2023
2afe031
Upgrade ecto dependency
javierg Oct 31, 2023
c858e1a
link db connection genserver to main supervisor
javierg Nov 8, 2023
3ff8907
push version
javierg Nov 8, 2023
86ce743
improve couchx adapter stability
javierg Nov 15, 2023
c9ba558
fix returning doc revs and ids
javierg Dec 7, 2023
537dc18
add multiple options
javierg Feb 23, 2024
9f66255
push version
javierg Feb 23, 2024
725a598
Merge pull request #15 from javierg/add-multiple-operators-queries
javierg Feb 23, 2024
b12c785
update docs
javierg Feb 23, 2024
f10e2ab
Fix Repo registry lookup in MangoIndex module (#16)
alejandrodevs Mar 8, 2024
760b47e
filter out nil docs from view parser
javierg Mar 8, 2024
89144a4
Merge pull request #17 from javierg/handle-nil-docs-in-viewreturn
javierg Mar 8, 2024
af6972b
Merge pull request #18 from javierg/v0.4
javierg Mar 8, 2024
f356203
push app to tag version 0.4.5
javierg Mar 9, 2024
339e24d
update docs
javierg Mar 9, 2024
eece756
remove documentation from repo
javierg Mar 11, 2024
a1a6213
ignore docs
javierg Mar 11, 2024
8973134
Support for interpolating variables in options (#19)
louismanson Apr 29, 2024
865533b
push app version to 0.4.6
javierg Apr 30, 2024
d89bbd4
restore support for in operator over ids
javierg May 2, 2024
1ce3702
push version to 0.4.7
javierg May 2, 2024
115c83b
handle ranges
javierg Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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]
...
```

Expand All @@ -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

Expand All @@ -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
Expand Down
166 changes: 124 additions & 42 deletions lib/couchx/adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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},
...
]
Expand Down Expand Up @@ -128,14 +127,15 @@ defmodule Couchx.Adapter do
```

Removing the documents from the database.

"""
import Couchx.CouchId

@behaviour Ecto.Adapter
@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
Expand Down Expand Up @@ -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

Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -358,31 +369,73 @@ 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
Enum.reduce(property, %{}, &process_property(&1, &2, values))
|> 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
Expand Down Expand Up @@ -420,46 +473,56 @@ 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

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

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion lib/couchx/constraint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading