CRUD collection abstraction with transactional support.
Provides a generic Collection type that wraps a DataSource backend and implements ILookup, Seqable, Counted for pattern-based access.
(def src (atom-source))
(def items (collection src))
(get items {:id 3}) ; fetch by query
(seq items) ; list all
(mutate! items nil data) ; create
(mutate! items {:id 3} data) ; update
(mutate! items {:id 3} nil) ; delete
For queries containing mutations, apply all mutations atomically
via transact!, then query the new state via snapshot:
(def src (atom-source))
;; Apply mutations atomically
(transact! src
[{:op :create :data {:title "Post 1"}}
{:op :create :data {:title "Post 2"}}])
;; Query the new state
(count (snapshot src)) ;=> 2
For multiple collections, call transact! on each, then snapshot:
(def sources {:posts (atom-source) :users (atom-source)})
(transact! (:users sources) [{:op :create :data {:name "Alice"}}])
(transact! (:posts sources) [{:op :create :data {:title "Hello"}}])
{:users (vals (snapshot (:users sources)))
:posts (vals (snapshot (:posts sources)))}
CRUD collection abstraction with transactional support.
Provides a generic Collection type that wraps a DataSource backend
and implements ILookup, Seqable, Counted for pattern-based access.
## Basic Usage
```clojure
(def src (atom-source))
(def items (collection src))
(get items {:id 3}) ; fetch by query
(seq items) ; list all
(mutate! items nil data) ; create
(mutate! items {:id 3} data) ; update
(mutate! items {:id 3} nil) ; delete
```
## Transactional Mutations
For queries containing mutations, apply all mutations atomically
via `transact!`, then query the new state via `snapshot`:
```clojure
(def src (atom-source))
;; Apply mutations atomically
(transact! src
[{:op :create :data {:title "Post 1"}}
{:op :create :data {:title "Post 2"}}])
;; Query the new state
(count (snapshot src)) ;=> 2
```
For multiple collections, call `transact!` on each, then `snapshot`:
```clojure
(def sources {:posts (atom-source) :users (atom-source)})
(transact! (:users sources) [{:op :create :data {:name "Alice"}}])
(transact! (:posts sources) [{:op :create :data {:title "Hello"}}])
{:users (vals (snapshot (:users sources)))
:posts (vals (snapshot (:posts sources)))}
```(atom-source)(atom-source {:keys [id-key initial] :or {id-key :id}})Create an atom-backed transactional data source.
Implements both DataSource (for individual CRUD) and TxSource (for atomic batch mutations).
Options:
Note: IDs must be positive integers. Auto-generated IDs start from 1 and increment. If providing initial data, all keys must be integers.
Examples:
(def src (atom-source))
(def src (atom-source {:id-key :user-id}))
(def src (atom-source {:initial {1 {:id 1 :name "Alice"}}}))
(def src (atom-source {:initial [{:id 10 :name "Bob"}]}))
Create an atom-backed transactional data source.
Implements both DataSource (for individual CRUD) and TxSource
(for atomic batch mutations).
Options:
- :id-key - Primary key field (default :id)
- :initial - Initial data as map {id -> item} or vector [item ...]
Note: IDs must be positive integers. Auto-generated IDs start from 1
and increment. If providing initial data, all keys must be integers.
Examples:
```clojure
(def src (atom-source))
(def src (atom-source {:id-key :user-id}))
(def src (atom-source {:initial {1 {:id 1 :name "Alice"}}}))
(def src (atom-source {:initial [{:id 10 :name "Bob"}]}))
```(collection data-source)(collection data-source {:keys [id-key indexes]})Create a Collection wrapping a DataSource.
Options:
The collection implements:
Create a Collection wrapping a DataSource.
Options:
- :id-key - Primary key field (default :id). Must match data-source config.
- :indexes - Set of indexed field sets for queries.
Default: #{#{<id-key>}} (primary key only)
Example: #{#{:id} #{:author} #{:status :type}}
The collection implements:
- ILookup: (get coll {:id 3}) -> fetch by query
- Seqable: (seq coll) -> list all
- Counted: (count coll) -> count all
- Mutable: (mutate! coll query value) -> create/update/delete
- Wireable: automatically serializes to vector for wire formatProtocol for CRUD data source backends.
Implement this for your storage layer (database, atom, API, etc.).
Protocol for CRUD data source backends. Implement this for your storage layer (database, atom, API, etc.).
(create! this data)Create new item. Returns created item with any generated fields.
Create new item. Returns created item with any generated fields.
(delete! this query)Delete item matching query. Returns true if deleted, false otherwise.
Delete item matching query. Returns true if deleted, false otherwise.
(fetch this query)Fetch item(s) matching query map. Returns item or nil.
Fetch item(s) matching query map. Returns item or nil.
(list-all this)List all items. Returns sequence.
List all items. Returns sequence.
(update! this query data)Update item matching query. Returns updated item or nil.
Update item matching query. Returns updated item or nil.
(lookup field-map)Create an ILookup + Wireable from a keyword->value map.
Delay values are dereferenced transparently on access.
->wire produces a plain map with all delays forced.
Use for non-enumerable resources where fields come from multiple sources with different costs:
(lookup {:id user-id ; cheap — used as-is
:email (:email session) ; cheap — used as-is
:slug (delay (db-lookup conn user-id)) ; expensive — computed once
:roles (or (:roles session) #{})}) ; cheap — used as-is
Delays are shared between ILookup and ->wire — a DB query runs at most once regardless of access path.
Create an ILookup + Wireable from a keyword->value map.
Delay values are dereferenced transparently on access.
`->wire` produces a plain map with all delays forced.
Use for non-enumerable resources where fields come from
multiple sources with different costs:
```clojure
(lookup {:id user-id ; cheap — used as-is
:email (:email session) ; cheap — used as-is
:slug (delay (db-lookup conn user-id)) ; expensive — computed once
:roles (or (:roles session) #{})}) ; cheap — used as-is
```
Delays are shared between ILookup and ->wire — a DB query
runs at most once regardless of access path.Protocol for collections that support CRUD mutations.
Protocol for collections that support CRUD mutations.
(mutate! coll query value)Perform mutation based on query and value:
Perform mutation based on query and value: - (mutate! coll nil data) -> CREATE - (mutate! coll query data) -> UPDATE - (mutate! coll query nil) -> DELETE
(read-only coll)Wrap a collection to make it read-only.
The returned collection supports ILookup, Seqable, Counted, and Wireable, but NOT Mutable. Attempts to mutate via pattern will fail with 'collection not mutable' error.
Use this for public API endpoints where reads are allowed but writes should be gated behind authentication.
Example:
(def posts (collection (atom-source)))
(def public-posts (read-only posts))
(seq public-posts) ; works
(get public-posts {:id 1}) ; works
(mutate! public-posts ...) ; throws - Mutable not implemented
Wrap a collection to make it read-only.
The returned collection supports ILookup, Seqable, Counted, and Wireable,
but NOT Mutable. Attempts to mutate via pattern will fail with
'collection not mutable' error.
Use this for public API endpoints where reads are allowed but writes
should be gated behind authentication.
Example:
```clojure
(def posts (collection (atom-source)))
(def public-posts (read-only posts))
(seq public-posts) ; works
(get public-posts {:id 1}) ; works
(mutate! public-posts ...) ; throws - Mutable not implemented
```Protocol for transactional data sources.
Extends DataSource concept with ability to apply multiple mutations atomically and snapshot state for querying.
Protocol for transactional data sources. Extends DataSource concept with ability to apply multiple mutations atomically and snapshot state for querying.
(snapshot this)Get immutable snapshot of current state for querying.
Get immutable snapshot of current state for querying.
(transact! this mutations)Apply mutations atomically. Returns snapshot after mutations.
Each mutation is a map: {:op :create | :update | :delete :query map (for update/delete) :data map (for create/update)}
All mutations succeed or none do.
Apply mutations atomically. Returns snapshot after mutations.
Each mutation is a map:
{:op :create | :update | :delete
:query map (for update/delete)
:data map (for create/update)}
All mutations succeed or none do.Protocol for types needing custom wire serialization.
Implement this for custom types that should be converted to standard Clojure data for Transit/EDN serialization.
Protocol for types needing custom wire serialization. Implement this for custom types that should be converted to standard Clojure data for Transit/EDN serialization.
(->wire this)Convert to serializable Clojure data (maps, vectors, etc.)
Convert to serializable Clojure data (maps, vectors, etc.)
(wrap-mutable coll mutate-fn)Wrap a collection with custom mutation logic, delegating reads.
mutate-fn receives (coll query value) and should return:
Reads (ILookup, Seqable, Counted) and Wireable delegate to the inner collection. Use this to add authorization, ownership checks, or field injection without reimplementing the full deftype boilerplate.
Example:
(def member-posts
(wrap-mutable posts
(fn [posts query value]
(if (owns? user-id query)
(mutate! posts query value)
{:error {:type :forbidden}}))))
Wrap a collection with custom mutation logic, delegating reads.
mutate-fn receives (coll query value) and should return:
- For create (nil query, some value): the created item
- For update (some query, some value): the updated item
- For delete (some query, nil value): true/false
- For errors: {:error {:type ... :message ...}}
Reads (ILookup, Seqable, Counted) and Wireable delegate to the
inner collection. Use this to add authorization, ownership checks,
or field injection without reimplementing the full deftype boilerplate.
Example:
```clojure
(def member-posts
(wrap-mutable posts
(fn [posts query value]
(if (owns? user-id query)
(mutate! posts query value)
{:error {:type :forbidden}}))))
```cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |