A reusable auction engine for Elixir. It runs more than one product on one engine. The auctioned item is generic. The captured contact details are a generic map that each product defines. The storage backend is a plug-in.
This library holds the hard code: the row locks, the one-pending rule, the raise math, the accept and reject state machine, the daily limit, and the asset upload order. It owns the schemas but not the database. The host app gives the repo and the settings.
Add a git dependency (pin a release tag):
{:auction_kit, github: "oshi-connect/auction_kit", tag: "v0.1.0"}config :auction_kit,
repo: MyApp.Repo,
auction_schema: MyApp.Auction,
item_schema: MyApp.Item,
bid_schema: MyApp.Bid,
storage_impl: AuctionKit.Storage.S3,
asset_key_prefix: "assets/myapp/",
max_asset_bytes: 5_000_000,
allowed_content_types: ~w(image/png image/jpeg image/jpg),
max_submissions_per_day: 10,
contact_spec: [
%{key: "website", required: false, normalize: {AuctionKit.Contact, :website}},
%{key: "twitter", required: false},
%{key: "discord", required: false}
],
# Optional. The engine calls this after a write, so you can broadcast.
on_change: {MyAppWeb.Endpoint, :broadcast_wall}An existing product can keep its own tables and columns. Override only the names
that differ, in AuctionKit.Mapping:
config :auction_kit,
bid_asset_key_field: :image_key,
bid_item_fk: :spot_id,
bid_item_assoc: :spot,
pending_unique_constraint: "bids_one_pending_per_spot"A greenfield product uses the schema macros. The macros give the generic shape.
defmodule MyApp.Auction do
use AuctionKit.Schema.Auction, table: "auctions", item_schema: MyApp.Item
end
defmodule MyApp.Item do
use AuctionKit.Schema.Item,
table: "items",
auction_schema: MyApp.Auction,
bid_schema: MyApp.Bid
end
defmodule MyApp.Bid do
use AuctionKit.Schema.Bid, table: "bids", item_schema: MyApp.Item
endCreate the tables from a migration:
defmodule MyApp.Repo.Migrations.CreateAuction do
use Ecto.Migration
def up, do: AuctionKit.Migrations.up()
def down, do: AuctionKit.Migrations.down()
endauction = MyApp.Repo.get_by!(MyApp.Auction, slug: "spring-sale")
AuctionKit.Auctions.place_bid(auction, %AuctionKit.PlaceBid{
item_id: item_id,
amount_cents: 2500,
display_name: "Kai",
email: "kai@example.com",
contact: %{"website" => "example.com"},
asset_bytes: bytes,
content_type: "image/png"
})
AuctionKit.Auctions.accept_bid(bid_id)
AuctionKit.Auctions.reject_bid(bid_id)
AuctionKit.Auctions.snapshot(auction, codes: ["I01", "I02"], visitor_count: 3)
AuctionKit.Auctions.list_pending_bids()The store is a plug-in. Pick one with :storage_impl. The engine calls the
store only through AuctionKit.Storage, so a product swaps the backend with a
config change and no code change.
| Adapter | Use |
|---|---|
AuctionKit.Storage.S3 |
Cloudflare R2, AWS S3, or MinIO. Needs ex_aws and ex_aws_s3. |
AuctionKit.Storage.Postgres |
A Postgres table, through Postgrex. No extra dependency. |
AuctionKit.Storage.Memory |
An ETS table, for tests and simple setups. |
A product may add any module that follows the AuctionKit.Storage behaviour.
For the Postgres backend, add the object table:
def up, do: AuctionKit.Storage.Postgres.Migration.up()The library is general. A second product uses a different contact spec and different item attributes. For example, a standee auction:
config :auction_kit,
storage_impl: AuctionKit.Storage.Postgres,
asset_key_prefix: "assets/standee/",
contact_spec: [
%{key: "company", required: true},
%{key: "phone", required: true},
%{key: "website", required: false, normalize: {AuctionKit.Contact, :website}}
]The item stores its own fields in the attributes map, for example
%{"floor" => 2, "aisle" => "A"}.
The library has its own test repo and an Ecto sandbox. It needs a running Postgres.
mix deps.get
mix test