joshrieken/plasm
{ "createdAt": "2016-01-07T23:02:38Z", "defaultBranch": "master", "description": "Ecto's composable query multitool (.count, .random, .earliest, .latest, .find, .at, .on, etc.)", "fullName": "joshrieken/plasm", "homepage": "", "language": "Elixir", "name": "plasm", "pushedAt": "2020-06-09T17:39:57Z", "stargazersCount": 91, "topics": [], "updatedAt": "2025-03-14T14:14:47Z", "url": "https://github.com/joshrieken/plasm"}A generic composable query library for Ecto.
:heart::heart::heart: Ecto, :cry::cry::cry: because I have to implement my own composable query functions for things like counting records, getting a random record and whatnot in all my models/projects.
NO MORE.
Plasm provides a set of generic, composable, higher-level functions that make working with Ecto more joyful and productive.
Design Objectives
Section titled “Design Objectives”- Work alongside
Ecto.Queryso both can beimported without conflict - Avoid reimplementing basic
Ecto.Queryfunctionality where possible - Provide syntactic sugar for common queries (e.g., see
countanddistinct_by) - Easy integration with Phoenix
- Permissive API (e.g., most functions that accept an atom will alternatively accept a string)
Examples
Section titled “Examples”Instead of writing this in your model:
defmodule MyGhostBustingApp.ProtonPack ...
def count(query) do for q in query, select: count(q.id) end
...endAnd using it this way:
ProtonPack |> ProtonPack.count |> Repo.oneJust use Plasm:
ProtonPack |> Plasm.count |> Repo.oneMore examples:
PkeMeter |> Plasm.later_than(:updated_at, "2016-01-04T14:00:00Z") |> Repo.allGhostTrap |> Plasm.find([3,6,9]) |> Repo.allStayPuftMarshmallowMan |> Plasm.random |> Repo.oneUsing in Models
Section titled “Using in Models”You can import Plasm and use it directly in your models:
defmodule MyApp.SomeModel do import Ecto.Query import Plasm
...
def random_distinct_names_by_order_of_insertion(query, n) do query |> order_by(asc: :inserted_at) |> distinct_by(:name) |> random(n) endendUsing with Phoenix
Section titled “Using with Phoenix”If you want Plasm to be universally accessible in all your Phoenix models, you can add it to web.ex:
defmodule MyApp.Web do ...
def model do quote do ...
import Plasm end endendPlasm.at(query, field_name, ecto_date_time)Plasm.at_or_later_than(query, field_name, ecto_date_time)Plasm.at_or_earlier_than(query, field_name, ecto_date_time)Plasm.average(query, field_name)Plasm.count(query)Plasm.count_distinct(query, field_name)Plasm.distinct_by(query, field_name)Plasm.earlier_than(query, field_name, ecto_date_or_date_time)Plasm.earliest(query, field_name)Plasm.earliest(query, field_name, n)Plasm.find(query, primary_key)Plasm.find(query, primary_keys)Plasm.later_than(query, field_name, ecto_date_or_date_time)Plasm.latest(query, field_name)Plasm.latest(query, field_name, n)Plasm.maximum(query, field_name)Plasm.minimum(query, field_name)Plasm.on(query, field_name, ecto_date)Plasm.on_or_later_than(query, field_name, ecto_date)Plasm.on_or_earlier_than(query, field_name, ecto_date)Plasm.random(query)Plasm.random(query, n)Plasm.total(query, field_name)Plasm.where_all(query, field_names_and_values)Plasm.where_none(query, field_names_and_values)Why not use shorter names, like avg, sum, etc.?
Section titled “Why not use shorter names, like avg, sum, etc.?”To avoid conflicts when importing. For instance, Plasm.min\2 and Plasm.max\2 would conflict with Kernel.min\2 and Kernel.max\2, so if you’re importing them, you’d need to prefix your calls with __MODULE__; e.g., __MODULE__.min(query, field_name). This sucks, so I chose to go with the longer versions and tried to stay consistent.
Note On DB Support
Section titled “Note On DB Support”Guaranteed to work with Postgres; others might work but haven’t been tested.
Note On Query Syntaxes
Section titled “Note On Query Syntaxes”Ecto supports two query syntaxes, keyword and query expression.
Example of the keyword syntax:
def for_name_or_age(query, name, age) do for x in query, where: x.name == ^name or x.age == ^ageendExample of the query expression syntax:
def for_name_or_age(query, name, age) do query |> where([x], x.name == ^name or x.age == ^age)endThe keyword syntax is a bit easier on the eyes, but is not fully compatible with Plasm. A case can be made for sticking with the query expression syntax for all functions that are meant to be composable, and especially if you plan to use Plasm or something like it.
Inspiration
Section titled “Inspiration”Many thanks to Drew Olson for his talk at ElixirConf 2015 and insightful blog post on the subject of composable Ecto queries.
Also thanks to Henrik Nyh for his Ectoo project, which has similar aims.
- Tests
- Hex docs
Installation
Section titled “Installation”Add Plasm to your list of dependencies in mix.exs:
def deps do [{:plasm, "~> 2.0"}]endEnsure Plasm is started before your application:
def application do [ applications: [ ... :plasm ... ] ]endIf you want to be on the bleeding edge, track the master branch of this repo:
{:plasm, git: "https://github.com/facto/plasm.git", branch: "master"}Copyright and License
Section titled “Copyright and License”Copyright (c) 2016-2020 Joshua Rieken.
Plasm source code is licensed under the Apache 2 License (see LICENSE.md).