├── .gitignore ├── LICENSE.md ├── README.md ├── package.json ├── pom.xml.asc ├── project.clj ├── resources └── public │ ├── css │ └── style.css │ └── index.html ├── src └── entitydb │ ├── core.cljs │ ├── relations.cljs │ └── util.cljs └── test └── entitydb ├── core.cljs └── test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .repl 13 | .nrepl-port 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2016 Mihael Konjevic (http://retroaktive.me) 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EntityDB 2 | 3 | This project is **DEPRECATED**. 4 | 5 | Last deployed version is [keechma/entitydb "0.1.6"]. 6 | 7 | New EntityDB is available at https://github.com/keechma/keechma-entitydb 8 | 9 | EntityDB is a library that handles storage of any kind of entities in your application. Entity is anything that is "identifiable". Usually the id attribute is used, but you can use anything that makes sense for your application. 10 | 11 | ## Documentation 12 | 13 | [Documentation](http://keechma.com/04-entitydb.html) and [API docs](http://keechma.com/api/keechma.edb.html). 14 | 15 | ## Setup 16 | 17 | ## License 18 | 19 | Copyright © 2016 Mihael Konjevic 20 | 21 | Distributed under the MIT License. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keechma", 3 | "version": "0.0.1", 4 | "description": "Frontend micro framework for ClojureScript and Reagent", 5 | "author": "", 6 | "license": "ISC", 7 | "devDependencies": { 8 | "karma": "^0.13.16", 9 | "karma-chrome-launcher": "^0.2.2", 10 | "karma-cljs-test": "^0.1.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pom.xml.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNATURE----- 2 | Version: GnuPG v1 3 | Comment: GPGTools - https://gpgtools.org 4 | 5 | iQEcBAABCgAGBQJaEEpKAAoJEJ5sZof6g9jcoL8H/1cpq9/EqGpLpdr5v8FdLOAe 6 | tva8yyIqEhXO4NIc4lx2GUuifBFWR2Xy7hHnmu/f1QXGvg5BarG6gahIVo2yJfF+ 7 | DtkGfXbtMEWQqFoB2I9wKk8pQUPtl+Ln+WINhDBUKRpjKsE60r1tuGGkWmqw1c5y 8 | PJKFvXKTlC/oGzn13yf1EOnmKXSfNTKTQnGyha1EiZZxmiyCzd7w7CPkx6I1pntC 9 | UXVbT29zHTxsWfDH2Lt5MbIJxsnRNGGpAa6SAf1yxiPP8sS60MdxIh6uh+Nen3CN 10 | FjAtfJI2EP10mvhNewonEBb7raCt9uQ0GfYLLmr2RnmjR5GCryI5Zw8+zmIKvmY= 11 | =TkP/ 12 | -----END PGP SIGNATURE----- 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject keechma/entitydb "0.1.6" 2 | :description "EntityDB - In memory entity store for ClojureScript applications." 3 | :url "http://keechma.com/" 4 | :license {:name "MIT"} 5 | 6 | :min-lein-version "2.5.3" 7 | 8 | :dependencies [[org.clojure/clojure "1.9.0-RC1"] 9 | [org.clojure/clojurescript "1.9.946"] 10 | [lein-doo "0.1.6"]] 11 | 12 | :plugins [[lein-figwheel "0.5.14"] 13 | [lein-cljsbuild "1.1.7"] 14 | [lein-doo "0.1.6"] 15 | [lein-codox "0.9.3"]] 16 | 17 | :source-paths ["src"] 18 | 19 | :codox {:language :clojurescript 20 | :metadata {:doc/format :markdown} 21 | :namespaces [entitydb.core]} 22 | 23 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 24 | 25 | :cljsbuild {:builds 26 | [{:id "dev" 27 | :source-paths ["src"] 28 | 29 | ;; If no code is to be run, set :figwheel true for continued automagical reloading 30 | :figwheel {:on-jsload "entitydb.core/on-js-reload"} 31 | 32 | :compiler {:main entitydb.core 33 | :asset-path "js/compiled/out" 34 | :output-to "resources/public/js/compiled/entitydb.js" 35 | :output-dir "resources/public/js/compiled/out" 36 | :source-map-timestamp true}} 37 | ;; This next build is an compressed minified build for 38 | ;; production. You can build this with: 39 | ;; lein cljsbuild once min 40 | {:id "min" 41 | :source-paths ["src"] 42 | :compiler {:output-to "resources/public/js/compiled/entitydb.js" 43 | :main entitydb.core 44 | :optimizations :advanced 45 | :pretty-print false}} 46 | {:id "test" 47 | :source-paths ["src" "test"] 48 | :compiler {:output-to 49 | "resources/public/js/compiled/test.js" 50 | :optimizations :none 51 | :main entitydb.test.test}}]} 52 | 53 | :figwheel {;; :http-server-root "public" ;; default and assumes "resources" 54 | ;; :server-port 3449 ;; default 55 | ;; :server-ip "127.0.0.1" 56 | 57 | :css-dirs ["resources/public/css"] ;; watch and update CSS 58 | 59 | ;; Start an nREPL server into the running figwheel process 60 | ;; :nrepl-port 7888 61 | 62 | ;; Server Ring Handler (optional) 63 | ;; if you want to embed a ring handler into the figwheel http-kit 64 | ;; server, this is for simple ring servers, if this 65 | ;; doesn't work for you just run your own server :) 66 | ;; :ring-handler hello_world.server/handler 67 | 68 | ;; To be able to open files in your editor from the heads up display 69 | ;; you will need to put a script on your path. 70 | ;; that script will have to take a file path and a line number 71 | ;; ie. in ~/bin/myfile-opener 72 | ;; #! /bin/sh 73 | ;; emacsclient -n +$2 $1 74 | ;; 75 | ;; :open-file-command "myfile-opener" 76 | 77 | ;; if you want to disable the REPL 78 | ;; :repl false 79 | 80 | ;; to configure a different figwheel logfile path 81 | ;; :server-logfile "tmp/logs/figwheel-logfile.log" 82 | }) 83 | -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | /* some style */ 2 | 3 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Figwheel template

11 |

Checkout your developer console.

12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/entitydb/core.cljs: -------------------------------------------------------------------------------- 1 | (ns entitydb.core 2 | (:require [entitydb.util :as util :refer [update-values]] 3 | [entitydb.relations :as relations] 4 | [clojure.set :as set])) 5 | 6 | (declare get-item-by-id) 7 | (declare insert-named-item) 8 | (declare insert-collection) 9 | (declare insert-related) 10 | (declare get-named-item) 11 | (declare get-collection) 12 | (declare insert-meta) 13 | (declare remove-meta) 14 | (declare get-collection-meta) 15 | (declare remove-collection-or-named-item) 16 | (declare remove-collection) 17 | 18 | (def ^:private meta-store :__meta-store__) 19 | 20 | (def relations :entitydb/relations) 21 | 22 | (defn insert-item 23 | "Inserts an item into the EntityDB collection. 24 | 25 | ```clojure 26 | (def schema {:foos {:id :id}}) 27 | (def entity-db-v1 {}) 28 | 29 | (def item {:id 1 :name \"Foo\"}) 30 | (def item-meta {:is-loading false}) 31 | 32 | (def entity-db-v2 (insert-item schema entity-db-v1 :foos item item-meta)) 33 | ;; Returns the new version of the entity-db with the item inserted 34 | ;; inserted into the store 35 | ``` 36 | " 37 | ([schema db entity-kw item] (insert-item schema db entity-kw item nil)) 38 | ([schema db entity-kw item meta] 39 | (let [id (util/get-item-id schema entity-kw item) 40 | relations (relations/get-relations schema entity-kw) 41 | db-with-inserted-relations (insert-related schema db relations entity-kw id item) 42 | processed-item (relations/remove-related-from-item 43 | (keys relations) 44 | (util/call-middleware-set schema entity-kw item)) 45 | merged-item (merge (or (get-in db [entity-kw :store id]) {}) processed-item)] 46 | (-> db-with-inserted-relations 47 | (insert-meta entity-kw id meta) 48 | (assoc-in [entity-kw :store id] merged-item))))) 49 | 50 | (defn insert-item-when-not-nil 51 | "Inserts an entity into the EntityDB if the entity is not nil." 52 | [schema db entity-kw item] 53 | (if-not (nil? item) 54 | (insert-item schema db entity-kw item) 55 | db)) 56 | 57 | (defn insert-named-item 58 | "Inserts an item into the EntityDB, and references it from the named item slot. 59 | 60 | Item will be stored in the internal store, and named item slot will contain only 61 | the identity of the item. 62 | 63 | ```clojure 64 | (def entity-db-v1 {}) 65 | (def schema {:foos {:id :id}}) 66 | 67 | (def entity-db-v2 (insert-named-item schema entity-db-v1 :foos :current {:id 1 :name \"foo\"})) 68 | ;; Returns the new version of the entity-db with the entity saved in the store and 69 | ;; referenced from the `:current` named item slot. 70 | 71 | (get-named-item schema entity-db-v2 :foos :current) 72 | ;; Returns the entity referenced from the `:current` named slot. 73 | 74 | ``` 75 | " 76 | ([schema db entity-kw collection-key item] 77 | (insert-named-item schema db entity-kw collection-key item nil)) 78 | ([schema db entity-kw collection-key item meta] 79 | (if (and (nil? item) (nil? meta)) 80 | db 81 | (let [id (util/get-item-id schema entity-kw item) 82 | meta-key (if (nil? item) collection-key id)] 83 | (-> db 84 | (remove-meta entity-kw collection-key) 85 | (assoc-in [entity-kw :c-one collection-key] id) 86 | ((partial insert-item-when-not-nil schema) entity-kw item) 87 | (insert-meta entity-kw meta-key meta)))))) 88 | 89 | (defn insert-collection 90 | "Inserts a collection of items into the EntityDB. Each item will be 91 | stored in the internal store map, and the collection will be stored as a vector 92 | of entity identities. 93 | 94 | ```clojure 95 | (def entity-db-v1 {}) 96 | (def schema {:foos {:id :id}}) 97 | 98 | (def collection [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}]) 99 | 100 | (def entity-db-v2 (insert-collection schema entity-db-v1 :foos :list collection)) 101 | ;; Returns the new version of entity db. Each item will be stored 102 | ;; in the internal store map and collection will contain only the 103 | ;; item ids. 104 | 105 | (get-collection schema entity-db-v2 :foos :list) 106 | ;; Returns a collection of items named `:list`. Although internally collections 107 | ;; stores only a vector of ids, this function will return a vector of entities. 108 | ;; 109 | ;; [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}] 110 | 111 | ``` 112 | " 113 | ([schema db entity-kw collection-key data] 114 | (insert-collection schema db entity-kw collection-key data nil)) 115 | ([schema db entity-kw collection-key data meta] 116 | (if (and (empty? data) (nil? meta)) 117 | (remove-collection db entity-kw collection-key) 118 | (let [id-fn (util/get-id-fn schema entity-kw) 119 | ids (into [] (map id-fn data))] 120 | (-> db 121 | (assoc-in [entity-kw :c-many collection-key] ids) 122 | (insert-meta entity-kw collection-key meta) 123 | (as-> db (reduce (fn [db item] 124 | (insert-item schema db entity-kw item)) 125 | db data))))))) 126 | 127 | (defn append-collection 128 | "Appends items to an existing collection. 129 | 130 | ```clojure 131 | (def entity-db-v1 {}) 132 | (def schema {:foos {:id :id}}) 133 | 134 | (def collection [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}]) 135 | 136 | (def entity-db-v2 (insert-collection schema entity-db-v1 :foos :list collection)) 137 | ;; Returns the new version of entity db. Each item will be stored 138 | ;; in the internal store map and collection will contain only the 139 | ;; item ids. 140 | 141 | (get-collection schema entity-db-v2 :foos :list) 142 | ;; Returns a collection of items named `:list`. Although internally collections 143 | ;; stores only a vector of ids, this function will return a vector of entities. 144 | ;; 145 | ;; [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}] 146 | 147 | 148 | (def entity-db-v3 (append-collection schema entity-db-v2 :foos :list [{:id 3 :name \"baz}])) 149 | 150 | (get-collection schema entity-db-v3 :foos :list) 151 | ;; Returns [{:id 1 :name \"foo\"} {:id 2 :name \"bar} {:id 3 :name \"baz\"}] 152 | 153 | ``` 154 | " 155 | ([schema db entity-kw collection-key data] 156 | (let [current-meta (get-collection-meta schema db entity-kw collection-key)] 157 | (append-collection schema db entity-kw collection-key data current-meta))) 158 | ([schema db entity-kw collection-key data meta] 159 | (let [c-path [entity-kw :c-many collection-key] 160 | current-ids (get-in db c-path) 161 | db-with-items (insert-collection schema db entity-kw collection-key data meta) 162 | new-ids (get-in db-with-items c-path)] 163 | (assoc-in db-with-items c-path (remove nil? (flatten [current-ids new-ids])))))) 164 | 165 | (defn prepend-collection 166 | "Prepends items to an existing collection. 167 | 168 | ```clojure 169 | (def entity-db-v1 {}) 170 | (def schema {:foos {:id :id}}) 171 | 172 | (def collection [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}]) 173 | 174 | (def entity-db-v2 (insert-collection schema entity-db-v1 :foos :list collection)) 175 | ;; Returns the new version of entity db. Each item will be stored 176 | ;; in the internal store map and collection will contain only the 177 | ;; item ids. 178 | 179 | (get-collection schema entity-db-v2 :foos :list) 180 | ;; Returns a collection of items named `:list`. Although internally collections 181 | ;; stores only a vector of ids, this function will return a vector of entities. 182 | ;; 183 | ;; [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}] 184 | 185 | 186 | (def entity-db-v3 (prepend-collection schema entity-db-v2 :foos :list [{:id 3 :name \"baz\"}])) 187 | 188 | (get-collection schema entity-db-v3 :foos :list) 189 | ;; Returns [{:id 3 :name \"baz\"} {:id 1 :name \"foo\"} {:id 2 :name \"bar\"}] 190 | 191 | ``` 192 | " 193 | ([schema db entity-kw collection-key data] 194 | (let [current-meta (get-collection-meta schema db entity-kw collection-key)] 195 | (prepend-collection schema db entity-kw collection-key data current-meta))) 196 | ([schema db entity-kw collection-key data meta] 197 | (let [c-path [entity-kw :c-many collection-key] 198 | current-ids (get-in db c-path) 199 | db-with-items (insert-collection schema db entity-kw collection-key data meta) 200 | new-ids (get-in db-with-items c-path)] 201 | (assoc-in db-with-items c-path (remove nil? (flatten [new-ids current-ids])))))) 202 | 203 | (defn ^:private insert-related [schema db relations entity-kw id item] 204 | (reduce-kv 205 | (fn [db relation-kw [relation-type related-entity-kw]] 206 | (let [collection-key (relations/get-related-collection-key entity-kw id relation-kw) 207 | relation-data (relation-kw item) 208 | remove-collection-type-map {:one :c-one :many :c-many} 209 | insert-collection-fn (if (= relation-type :one) 210 | insert-named-item 211 | insert-collection)] 212 | (cond 213 | (fn? relation-data) 214 | db 215 | 216 | (not (contains? item relation-kw)) 217 | db 218 | 219 | (and (contains? item relation-kw) (nil? (seq relation-data))) 220 | (remove-collection-or-named-item db related-entity-kw (remove-collection-type-map relation-type) collection-key) 221 | 222 | :else 223 | (insert-collection-fn schema db related-entity-kw collection-key relation-data)))) 224 | db relations)) 225 | 226 | (defn insert-meta 227 | "Inserts meta data for an entity or collection into the store." 228 | [db entity-kw meta-key meta] 229 | (let [schema {meta-store {:id (partial util/get-meta-id entity-kw meta-key)}}] 230 | (if (nil? meta) 231 | (remove-meta db entity-kw meta-key) 232 | (insert-item schema (util/add-empty-layout db meta-store) meta-store meta)))) 233 | 234 | (defn ^:private remove-item-id-from-named-items [collections id] 235 | (into {} (filter (fn [[key val]] 236 | (not= val id)) collections))) 237 | 238 | (defn ^:private remove-item-id-from-collections [collections id] 239 | (update-values collections (fn [val] 240 | (filterv (partial not= id) val)))) 241 | 242 | (defn remove-item 243 | "Removes item from the store. It will also remove it from any named-item slots or collections. 244 | 245 | ```clojure 246 | (def entity-db-v1 {}) 247 | (def schema {:foos {:id :id}}) 248 | 249 | (def foo-entity {:id 1 :name \"Bar\"}) 250 | 251 | ;; insert `foo-entity` in the `:current` named item slot 252 | (def entity-db-v2 (insert-named-item schema entity-db-v1 :foos :current foo-entity)) 253 | 254 | ;; insert `foo-entity` as a part of the `:list` collection 255 | (def entity-db-v3 (insert-collection schema entity-db-v2 :foos :list [foo-entity])) 256 | 257 | ;; get `foo-entity` from the entity-db 258 | (get-item-by-id schema entity-db-v3 :foos 1) 259 | ;; returns `foo-entity` 260 | 261 | (def entity-db-v4 (remove-item schema entity-db :foos 1)) 262 | 263 | (get-named-item schema entity-db-v4 :foos :current) 264 | ;; returns `nil` 265 | 266 | (get-collection schema entity-db-v4 :foos :list) 267 | ;; returns [] 268 | ``` 269 | " 270 | [schema db entity-kw id] 271 | (let [c-one-without-item-id (remove-item-id-from-named-items (get-in db [entity-kw :c-one]) id) 272 | c-many-without-item-id (remove-item-id-from-collections (get-in db [entity-kw :c-many]) id) 273 | store-without-item (dissoc (get-in db [entity-kw :store]) id) 274 | db (-> db 275 | (remove-meta entity-kw id) 276 | (assoc-in [entity-kw :store] store-without-item) 277 | (assoc-in [entity-kw :c-one] c-one-without-item-id) 278 | (assoc-in [entity-kw :c-many] c-many-without-item-id)) 279 | relations (relations/get-relations schema entity-kw)] 280 | (reduce-kv (partial relations/remove-related-collections entity-kw id) db relations))) 281 | 282 | (defn ^:private remove-collection-or-named-item [db entity-kw collection-type collection-key] 283 | (let [collections-without (dissoc (get-in db [entity-kw collection-type]) collection-key)] 284 | (-> db 285 | (remove-meta entity-kw collection-key) 286 | (assoc-in [entity-kw collection-type] collections-without)))) 287 | 288 | (defn remove-named-item 289 | "Removes the named-item slot. Entity will still be stored in the internal store, but 290 | won't be available through the named-item slot. 291 | 292 | ```clojure 293 | (def entity-db-v1 {}) 294 | (def schema {:foos {:id :id}}) 295 | 296 | (def foo-entity {:id 1 :name \"bar\"}) 297 | 298 | (def entity-db-v2 (insert-named-item schema entity-db-v1 :foos :current foo-entity)) 299 | 300 | (get-named-item schema entity-db-v1 :foos :current) 301 | ;; Returns `{:id 1 :name \"bar\"}` 302 | 303 | (def entity-db-v3 (remove-named-item schema entity-db-v2 :foos :current)) 304 | 305 | (get-named-item schema entity-db-v2 :foos :current) 306 | ;; Returns `nil` 307 | 308 | (get-item-by-id schema entity-db-v2 :foos 1) 309 | ;; Returns `{:id 1 :name \"bar\"}` 310 | ``` 311 | " 312 | [db entity-kw collection-key] 313 | (remove-collection-or-named-item db entity-kw :c-one collection-key)) 314 | 315 | (defn remove-collection 316 | "Removes the collection. Entities referenced from the collection will still be stored in 317 | the internal store, but won't be available through the collection API. 318 | 319 | ```clojure 320 | (def entity-db-v1 {}) 321 | (def schema {:foos {:id :id}}) 322 | 323 | (def foo-entity {:id 1 :name \"bar\"}) 324 | 325 | (def entity-db-v2 (insert-collection schema entity-db-v1 :foos :list [foo-entity])) 326 | 327 | (get-collection schema entity-db-v2 :foos :list) 328 | ;; Returns `[{:id 1 :name \"bar\"}]` 329 | 330 | (def entity-db-v3 (remove-collection schema entity-db-v2 :foos :list)) 331 | 332 | (get-collection schema entity-db-v2 :foos :list) 333 | ;; Returns `nil` 334 | 335 | (get-item-by-id schema entity-db-v2 :foos 1) 336 | ;; Returns `{:id 1 :name \"bar\"}` 337 | ``` 338 | " 339 | [db entity-kw collection-key] 340 | (remove-collection-or-named-item db entity-kw :c-many collection-key)) 341 | 342 | (defn empty-collection 343 | "Empties a collection, but leaves the meta intact. If the new meta is provided it will 344 | be merged into the current meta. Entities referenced from the collection will still be stored in 345 | the internal store, but won't be available through the collection API. 346 | 347 | ```clojure 348 | (def entity-db-v1 {}) 349 | (def schema {:foos {:id :id}}) 350 | 351 | (def foo-entity {:id 1 :name \"bar\"}) 352 | 353 | (def entity-db-v2 (insert-collection schema entity-db-v1 :foos :list [foo-entity])) 354 | 355 | (get-collection schema entity-db-v2 :foos :list) 356 | ;; Returns `[{:id 1 :name \"bar\"}]` 357 | 358 | (def entity-db-v3 (empty-collection schema entity-db-v2 :foos :list)) 359 | 360 | (get-collection schema entity-db-v2 :foos :list) 361 | ;; Returns `[]` 362 | 363 | (get-item-by-id schema entity-db-v2 :foos 1) 364 | ;; Returns `{:id 1 :name \"bar\"}` 365 | ``` 366 | " 367 | 368 | ([db entity-kw collection-key] 369 | (empty-collection db entity-kw collection-key {})) 370 | ([db entity-kw collection-key meta] 371 | (-> db 372 | (assoc-in [entity-kw :c-many collection-key] []) 373 | (insert-meta entity-kw collection-key meta)))) 374 | 375 | (defn remove-meta 376 | "Removes any meta data stored on the entity or collection" 377 | [db entity-kw id] 378 | (let [meta-key (util/get-meta-id entity-kw id) 379 | current-meta (get-in db [meta-store :store meta-key])] 380 | (if (nil? current-meta) 381 | db 382 | (let [store (get-in db [meta-store :store]) 383 | store-without-item (dissoc store meta-key)] 384 | (assoc-in db [meta-store :store] (or store-without-item {})))))) 385 | 386 | (defn get-item-meta 387 | "Gets meta data for an entity." 388 | [schema db entity-kw id] 389 | (if (= entity-kw meta-store) 390 | nil 391 | (get-item-by-id schema db meta-store (util/get-meta-id entity-kw id)))) 392 | 393 | (defn get-named-item-meta 394 | "Returns the meta data for an entity referenced in the named item slot." 395 | [schema db entity-kw collection-key] 396 | (let [item (get-named-item schema db entity-kw collection-key false) 397 | meta-key (if (nil? item) 398 | collection-key 399 | (util/get-item-id schema entity-kw item))] 400 | (get-item-meta schema db entity-kw meta-key))) 401 | 402 | (def get-collection-meta 403 | "Returns the meta data for a collection." 404 | get-item-meta) 405 | 406 | (defn ^:private get-related-items-fn [schema db entity-kw id] 407 | (fn [item relation-kw [relation-type related-entity-kw]] 408 | (let [collection-key (relations/get-related-collection-key entity-kw id relation-kw) 409 | get-related-fn (if (= relation-type :one) 410 | get-named-item 411 | get-collection) 412 | data-fn (partial get-related-fn schema db related-entity-kw collection-key)] 413 | (assoc item relation-kw data-fn)))) 414 | 415 | (defn resolve-relations [item pull-relations] 416 | (if (seq pull-relations) 417 | (reduce 418 | (fn [acc r] 419 | (let [relation (if (vector? r) r [r]) 420 | [attr & attr-pull-relations] relation 421 | attr-getter (get acc attr)] 422 | (if-let [resolved (when (fn? attr-getter) (attr-getter))] 423 | (if (vector? resolved) 424 | (assoc acc attr (map #(resolve-relations % attr-pull-relations) resolved)) 425 | (assoc acc attr (resolve-relations resolved attr-pull-relations))) 426 | acc))) 427 | item pull-relations) 428 | item)) 429 | 430 | (defn get-item-by-id 431 | "Gets an entity from the store by the id" 432 | ([schema db entity-kw id] (get-item-by-id schema db entity-kw id [])) 433 | ([schema db entity-kw id pull-relations] 434 | (let [relations (relations/get-relations schema entity-kw) 435 | item (get-in db [entity-kw :store id])] 436 | (if (nil? item) 437 | nil 438 | (-> item 439 | (with-meta (get-item-meta schema db entity-kw id)) 440 | (as-> item (reduce-kv (get-related-items-fn schema db entity-kw id) item relations)) 441 | (resolve-relations pull-relations)))))) 442 | 443 | (defn get-collection 444 | "Gets collection by it's key. Internally collections store only entity ids, but 445 | this function will return a collection of entities based on the ids stored in the collection 446 | 447 | 448 | ```clojure 449 | (def entity-db-v1 {}) 450 | (def schema {:foos {:id :id}}) 451 | 452 | (def collection [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}]) 453 | 454 | (def entity-db-v2 (insert-collection schema entity-db-v1 :foos :list collection)) 455 | ;; Returns the new version of entity db. Each item will be stored 456 | ;; in the internal store map and collection will contain only the 457 | ;; item ids. 458 | 459 | (get-collection schema entity-db-v2 :foos :list) 460 | ;; Returns a collection of items named `:list`. Although internally collections 461 | ;; stores only a vector of ids, this function will return a vector of entities. 462 | ;; 463 | ;; [{:id 1 :name \"foo\"} {:id 2 :name \"bar\"}] 464 | ``` 465 | " 466 | ([schema db entity-kw collection-key] 467 | (get-collection schema db entity-kw collection-key [])) 468 | ([schema db entity-kw collection-key pull-relations] 469 | (let [ids (get-in db [entity-kw :c-many collection-key])] 470 | (with-meta 471 | (into [] (map #(get-item-by-id schema db entity-kw % pull-relations) ids)) 472 | (get-collection-meta schema db entity-kw collection-key))))) 473 | 474 | (defn get-named-item 475 | "Gets an entity referenced from the named item slot. Internally named slots store 476 | only entity ids but this function will return an entity based on the id." 477 | ([schema db entity-kw collection-key] 478 | (get-named-item schema db entity-kw collection-key false [])) 479 | ([schema db entity-kw collection-key include-meta] 480 | (get-named-item schema db entity-kw collection-key include-meta [])) 481 | ([schema db entity-kw collection-key include-meta pull-relations] 482 | (let [id (get-in db [entity-kw :c-one collection-key]) 483 | item (get-item-by-id schema db entity-kw id pull-relations)] 484 | (if include-meta 485 | (with-meta 486 | item 487 | (get-named-item-meta schema db entity-kw collection-key)) 488 | item)))) 489 | 490 | (defn ^:private vacuum-entity-db [db entity-kw] 491 | (let [store (get-in db [entity-kw :store]) 492 | ids (keys store) 493 | locked-one-ids (vals (get-in db [entity-kw :c-one])) 494 | locked-many-ids (vals (get-in db [entity-kw :c-many])) 495 | locked-ids (flatten [locked-one-ids locked-many-ids]) 496 | to-remove-ids (set/difference (set ids) (set locked-ids)) 497 | db-without-meta (reduce (fn [db id] 498 | (remove-meta db entity-kw id)) 499 | db 500 | to-remove-ids)] 501 | (assoc-in db-without-meta 502 | [entity-kw :store] 503 | (select-keys store locked-ids)))) 504 | 505 | (defn vacuum 506 | "Removes orphaned entities from the EntityDB. Any entity that is not referenced 507 | in a collection or in a named item slot will be removed from the EntityDB" 508 | [db] 509 | (let [entity-kws (keys db) 510 | entity-kws-without-meta (filterv (fn [k] (not (= k meta-store))) entity-kws)] 511 | (reduce vacuum-entity-db db entity-kws-without-meta))) 512 | 513 | (defn get-relation-path [schema parent-entity-kw attr parent] 514 | [parent-entity-kw (util/get-item-id schema parent-entity-kw parent) attr]) 515 | 516 | (defn wrap-collection-fn-with-relation-path [relation-fn include-schema?] 517 | (fn [schema db parent-entity-kw attr parent & args] 518 | (let [relation (get-in schema [parent-entity-kw :relations attr]) 519 | relation-collection? (= :many (first relation)) 520 | relation-kw (last relation)] 521 | (when (not relation-collection?) 522 | (throw (ex-info (str attr " is not a collection relation") {}))) 523 | (let [path (get-relation-path schema parent-entity-kw attr parent) 524 | default-args (if include-schema? [schema db relation-kw path] [db relation-kw path])] 525 | (apply relation-fn (into default-args args)))))) 526 | 527 | 528 | (def insert-related-collection (wrap-collection-fn-with-relation-path insert-collection true)) 529 | (def remove-related-collection (wrap-collection-fn-with-relation-path remove-collection false)) 530 | (def prepend-related-collection (wrap-collection-fn-with-relation-path prepend-collection true)) 531 | (def append-related-collection (wrap-collection-fn-with-relation-path append-collection true)) 532 | 533 | 534 | (defn make-dbal 535 | "Returns a map with all public functions. These functions will have `schema` 536 | partially applied to them so you don't have to pass the schema around." 537 | [schema] 538 | {:insert-item (partial (util/ensure-layout insert-item) schema) 539 | :insert-named-item (partial (util/ensure-layout insert-named-item) schema) 540 | :insert-collection (partial (util/ensure-layout insert-collection) schema) 541 | :insert-related-collection (partial insert-related-collection schema) 542 | :append-collection (partial (util/ensure-layout append-collection) schema) 543 | :append-related-collection (partial append-related-collection schema) 544 | :prepend-collection (partial (util/ensure-layout prepend-collection) schema) 545 | :prepend-related-collection (partial prepend-related-collection schema) 546 | :insert-meta insert-meta 547 | :remove-item (partial (util/ensure-layout remove-item) schema) 548 | :remove-named-item remove-named-item 549 | :remove-collection remove-collection 550 | :remove-related-collection (partial remove-related-collection schema) 551 | :remove-meta remove-meta 552 | :get-item-by-id (partial get-item-by-id schema) 553 | :get-named-item (partial get-named-item schema) 554 | :get-collection (partial (util/ensure-layout get-collection) schema) 555 | :get-item-meta (partial get-item-meta schema) 556 | :get-named-item-meta (partial get-named-item-meta schema) 557 | :get-collection-meta (partial get-collection-meta schema) 558 | :vacuum vacuum}) 559 | -------------------------------------------------------------------------------- /src/entitydb/relations.cljs: -------------------------------------------------------------------------------- 1 | (ns entitydb.relations 2 | (:require [clojure.string :refer [join]] 3 | [entitydb.util :as util])) 4 | 5 | 6 | (defn get-relations [schema entity-kw] 7 | (or (get-in schema [entity-kw :relations]) {})) 8 | 9 | (defn get-related-collection-key [entity-kw id relation-kw] 10 | [entity-kw id relation-kw]) 11 | 12 | (defn remove-related-from-item [related-entity-kws item] 13 | (reduce (fn [item related-entity-kw] 14 | (dissoc item related-entity-kw)) item related-entity-kws)) 15 | 16 | 17 | (defn remove-related-collections [entity-kw id db relation-kw [relation-type related-entity-kw]] 18 | (let [collection-key (get-related-collection-key entity-kw id relation-kw) 19 | collection-type (if (= relation-type :one) 20 | :c-one 21 | :c-many) 22 | collections-without-related (dissoc (get-in db [related-entity-kw collection-type]) collection-key)] 23 | (assoc-in db [related-entity-kw collection-type] collections-without-related))) 24 | -------------------------------------------------------------------------------- /src/entitydb/util.cljs: -------------------------------------------------------------------------------- 1 | (ns entitydb.util) 2 | 3 | (defn passthrough-item [item] item) 4 | 5 | (defn add-empty-layout [db entity-kw] 6 | (if (nil? (entity-kw db)) 7 | (assoc db entity-kw {:store {} 8 | :c-one {} 9 | :c-many {}}) 10 | db)) 11 | 12 | (defn ensure-layout [dbal-fn] 13 | (fn [schema db entity-kw & args] 14 | (let [db-with-layout (add-empty-layout db entity-kw)] 15 | (apply dbal-fn (concat [schema db-with-layout entity-kw] args))))) 16 | 17 | 18 | (defn call-middleware [get-or-set schema entity-kw item] 19 | (let [middlewares (or (get-in schema [entity-kw :middleware get-or-set]) 20 | [passthrough-item]) 21 | pipeline (apply comp (reverse middlewares))] 22 | (pipeline item))) 23 | 24 | 25 | (def call-middleware-set (partial call-middleware :set)) 26 | (def call-middleware-get (partial call-middleware :get)) 27 | 28 | (defn get-id-fn [schema entity-kw] 29 | (or (get-in schema [entity-kw :id]) :id)) 30 | 31 | (defn get-item-id [schema entity-kw item] 32 | (let [id-fn (get-id-fn schema entity-kw)] 33 | (id-fn item))) 34 | 35 | (defn get-meta-id [entity-kw id] 36 | [entity-kw id]) 37 | 38 | (defn update-values [m f & args] 39 | (reduce (fn [r [k v]] (assoc r k (apply f v args))) {} m)) 40 | -------------------------------------------------------------------------------- /test/entitydb/core.cljs: -------------------------------------------------------------------------------- 1 | (ns entitydb.test.core 2 | (:require [cljs.test :refer-macros [deftest is]] 3 | [entitydb.core :as edb] 4 | [entitydb.util :as util] 5 | [entitydb.relations :as relations] 6 | [clojure.string :refer [upper-case]])) 7 | 8 | (def schema {:notes {:id :id 9 | :middleware {:set [(fn [item] item)]} 10 | :relations {:user [:one :users] 11 | :links [:many :links]} 12 | :schema {}} 13 | :links {:id :id} 14 | :users {:id :id}}) 15 | 16 | (def dbal (edb/make-dbal schema)) 17 | 18 | (defn uppercase-note-title [i] 19 | (assoc i :title (upper-case (:title i)))) 20 | 21 | (deftest ensure-layout [] 22 | (let [ensured-fn (util/ensure-layout (fn [schema db entity-kw] db)) 23 | new-db (ensured-fn dbal {} :notes)] 24 | (is (= new-db {:notes {:store {} 25 | :c-one {} 26 | :c-many {}}})))) 27 | 28 | (deftest call-middleware-set [] 29 | (let [processed-item (util/call-middleware-set 30 | {:notes {:middleware {:set [uppercase-note-title]}}} 31 | :notes 32 | {:title "note title"})] 33 | (is (= (:title processed-item) "NOTE TITLE")))) 34 | 35 | (deftest insert-item [] 36 | (let [db (util/add-empty-layout {} :notes) 37 | db-with-item (edb/insert-item {:notes {:id :id}} db :notes {:id 1 :title "Note title"})] 38 | (is (= (get-in db-with-item [:notes :store 1 :title]) "Note title")))) 39 | 40 | (deftest insert-item-with-custom-id-fn [] 41 | (let [db (util/add-empty-layout {} :notes) 42 | id-fn (fn [item] (str (:id item) "-note")) 43 | schema {:notes {:id id-fn}} 44 | db-with-item (edb/insert-item schema db :notes {:id 1 :title "Note title"})] 45 | (is (= (get-in db-with-item [:notes :store "1-note" :title]) "Note title")))) 46 | 47 | (deftest remove-item [] 48 | (let [db {:notes {:store {1 {:id 1 :title "Note title"}} 49 | :c-one {} 50 | :c-many {}} 51 | :users {:store {1 {:id 1 :username "Retro"}} 52 | :c-one {[:notes 1 :user] 1} 53 | :c-many {}} 54 | :links {:store {1 {:id 1 :url "www.google.com"}} 55 | :c-one {} 56 | :c-many {[:notes 1 :links] [1]}}} 57 | db-after-delete {:notes {:store {} 58 | :c-one {} 59 | :c-many {}} 60 | :users {:store {1 {:id 1 :username "Retro"}} 61 | :c-one {} 62 | :c-many {}} 63 | :links {:store {1 {:id 1 :url "www.google.com"}} 64 | :c-one {} 65 | :c-many {}}}] 66 | (is (= db-after-delete (edb/remove-item schema db :notes 1))))) 67 | 68 | (deftest get-item-by-id [] 69 | (let [db {:notes {:store {1 {:id 1 :title "Note title"}}}}] 70 | (is (= (:title (edb/get-item-by-id {} db :notes 1)) "Note title")))) 71 | 72 | (deftest insert-named-item [] 73 | (let [schema {:notes {}} 74 | db (util/add-empty-layout {} :notes) 75 | db-with-items (edb/insert-named-item schema db :notes :current-item {:id 1 :title "Note title"}) 76 | expected-layout {:notes {:store {1 {:id 1 :title "Note title"}} 77 | :c-one {:current-item 1} 78 | :c-many {}}}] 79 | (is (= db-with-items expected-layout)))) 80 | 81 | (deftest insert-collection [] 82 | (let [schema {:notes {}} 83 | db (util/add-empty-layout {} :notes) 84 | note-1 {:id 1 :title "Note title 1"} 85 | note-2 {:id 2 :title "Note title 2"} 86 | db-with-items (edb/insert-collection schema db :notes :latest [note-1 note-2]) 87 | expected-layout {:notes {:store {1 note-1 2 note-2} 88 | :c-one {} 89 | :c-many {:latest [1 2]}}}] 90 | (is (= db-with-items expected-layout)))) 91 | 92 | (deftest append-collection [] 93 | (let [schema {:notes {}} 94 | db {:notes {:store {1 {:id 1} 2 {:id 2}} 95 | :c-one {} 96 | :c-many {:latest [1 2]}}} 97 | expected-db {:notes {:store {1 {:id 1} 98 | 2 {:id 2} 99 | 3 {:id 3} 100 | 4 {:id 4}} 101 | :c-one {} 102 | :c-many {:latest [1 2 3 4]}}}] 103 | (is (= expected-db (edb/append-collection 104 | schema 105 | db 106 | :notes 107 | :latest 108 | [{:id 3} {:id 4}]))))) 109 | 110 | (deftest prepend-collection [] 111 | (let [schema {:notes {}} 112 | db {:notes {:store {1 {:id 1} 2 {:id 2}} 113 | :c-one {} 114 | :c-many {:latest [1 2]}}} 115 | expected-db {:notes {:store {1 {:id 1} 116 | 2 {:id 2} 117 | 3 {:id 3} 118 | 4 {:id 4}} 119 | :c-one {} 120 | :c-many {:latest [3 4 1 2]}}}] 121 | (is (= expected-db (edb/prepend-collection 122 | schema 123 | db 124 | :notes 125 | :latest 126 | [{:id 3} {:id 4}]))))) 127 | 128 | (deftest prepend-to-empty-collection [] 129 | (let [schema {:notes {}} 130 | db {:notes {:store {} 131 | :c-one {} 132 | :c-many {}}} 133 | expected-db {:notes {:store {1 {:id 1}} 134 | :c-one {} 135 | :c-many {:latest [1]}}}] 136 | (is (= expected-db (edb/prepend-collection 137 | schema 138 | db 139 | :notes 140 | :latest 141 | [{:id 1}]))))) 142 | 143 | (deftest append-to-empty-collection [] 144 | (let [schema {:notes {}} 145 | db {:notes {:store {} 146 | :c-one {} 147 | :c-many {}}} 148 | expected-db {:notes {:store {1 {:id 1}} 149 | :c-one {} 150 | :c-many {:latest [1]}}}] 151 | (is (= expected-db (edb/append-collection 152 | schema 153 | db 154 | :notes 155 | :latest 156 | [{:id 1}]))))) 157 | 158 | 159 | (deftest get-named-item [] 160 | (let [schema {:notes {}} 161 | note-1 {:id 1 :title "Note title 1"} 162 | db {:notes {:store {1 note-1} 163 | :c-one {:current 1} 164 | :c-many {}}}] 165 | (is (= (edb/get-named-item schema db :notes :current) note-1)))) 166 | 167 | (deftest get-collection [] 168 | (let [schema {:notes {}} 169 | note-1 {:id 1 :title "Note title 1"} 170 | note-2 {:id 2 :title "Note title 2"} 171 | db {:notes {:store {1 note-1 2 note-2} 172 | :c-one {} 173 | :c-many {:latest [1 2]}}}] 174 | (is (= (edb/get-collection schema db :notes :latest) [note-1 note-2])))) 175 | 176 | (deftest inserting-nil-relation-one-removes-existing-relation-one [] 177 | (let [schema {:users {} 178 | :notes {:relations {:user [:one :users]}}} 179 | db (-> {} 180 | (util/add-empty-layout :notes) 181 | (util/add-empty-layout :users)) 182 | note {:id 1 :user {:id 1}} 183 | note-with-nil-user {:id 1 :user nil} 184 | db-with-user (edb/insert-item schema db :notes note) 185 | expected-db-with-user {:notes {:store {1 {:id 1}} 186 | :c-one {} 187 | :c-many {}} 188 | :users {:store {1 {:id 1}} 189 | :c-one {[:notes 1 :user] 1} 190 | :c-many {}}} 191 | expected-db-with-nil-user {:notes {:store {1 {:id 1}} 192 | :c-one {} 193 | :c-many {}} 194 | :users {:store {1 {:id 1}} 195 | :c-one {} 196 | :c-many {}}}] 197 | (is (= db-with-user expected-db-with-user)) 198 | (is (= (edb/insert-item schema db-with-user :notes note-with-nil-user) expected-db-with-nil-user)))) 199 | 200 | (deftest inserting-item-with-no-related-data-leaves-existing-relations-intact [] 201 | (let [schema {:users {} 202 | :notes {:relations {:user [:one :users]}}} 203 | db (-> {} 204 | (util/add-empty-layout :notes) 205 | (util/add-empty-layout :users)) 206 | note {:id 1 :user {:id 1}} 207 | note-with-no-user {:id 1 :title "Foo"} 208 | db-with-user (edb/insert-item schema db :notes note) 209 | expected-db-with-user {:notes {:store {1 {:id 1}} 210 | :c-one {} 211 | :c-many {}} 212 | :users {:store {1 {:id 1}} 213 | :c-one {[:notes 1 :user] 1} 214 | :c-many {}}} 215 | expected-db-after-note-with-no-user {:notes {:store {1 {:id 1 :title "Foo"}} 216 | :c-one {} 217 | :c-many {}} 218 | :users {:store {1 {:id 1}} 219 | :c-one {[:notes 1 :user] 1} 220 | :c-many {}}}] 221 | (is (= db-with-user expected-db-with-user)) 222 | (is (= (edb/insert-item schema db-with-user :notes note-with-no-user) expected-db-after-note-with-no-user)))) 223 | 224 | (deftest relation-one [] 225 | (let [db (-> {} 226 | (util/add-empty-layout :notes) 227 | (util/add-empty-layout :users)) 228 | note {:id 1 :title "Note title" :user {:id 1 :username "Retro"}} 229 | db-with-items (edb/insert-item schema db :notes note) 230 | expected-db {:notes {:store {1 {:id 1 :title "Note title"}} 231 | :c-one {} 232 | :c-many {}} 233 | :users {:store {1 {:id 1 :username "Retro"}} 234 | :c-one {[:notes 1 :user] 1} 235 | :c-many {}}}] 236 | (is (= db-with-items expected-db)) 237 | (let [note-from-db (edb/get-item-by-id schema db-with-items :notes 1) 238 | user-from-db (edb/get-item-by-id schema db-with-items :users 1)] 239 | (is (= "Note title" (:title note-from-db))) 240 | (is (= 1 (:id note))) 241 | (is (= user-from-db ((:user note-from-db)))) 242 | (is (= user-from-db {:id 1 :username "Retro"}))))) 243 | 244 | (deftest relation-many [] 245 | (let [db (-> {} 246 | (util/add-empty-layout :notes) 247 | (util/add-empty-layout :links)) 248 | note {:id 1 :title "Note title" 249 | :links [{:id 1 :url "http://google.com"} 250 | {:id 2 :url "http://bing.com"}]} 251 | db-with-items (edb/insert-item schema db :notes note) 252 | expected-db {:notes {:store {1 {:id 1 :title "Note title"}} 253 | :c-one {} 254 | :c-many {}} 255 | :links {:store {1 {:id 1 :url "http://google.com"} 256 | 2 {:id 2 :url "http://bing.com"}} 257 | :c-one {} 258 | :c-many {[:notes 1 :links] [1 2]}}}] 259 | (is (= db-with-items expected-db)) 260 | (is (= (edb/get-item-by-id schema db-with-items :links 1) {:id 1 :url "http://google.com"})) 261 | (let [note-from-db (edb/get-item-by-id schema db-with-items :notes 1)] 262 | (is (= "Note title" (:title note-from-db))) 263 | (is (= 1 (:id note-from-db))) 264 | (is (= (:links note) ((:links note-from-db))))))) 265 | 266 | (deftest circular-relations 267 | (let [db (-> {} 268 | (util/add-empty-layout :notes) 269 | (util/add-empty-layout :links)) 270 | schema {:notes {:relations {:links [:many :links] 271 | :user [:one :user]}} 272 | :links {:relations {:user [:one :user] 273 | :notes [:many :notes]}}} 274 | note {:id 1 275 | :title "Note title" 276 | :links [{:id 1} {:id 2}]} 277 | link {:id 1 278 | :url "http://www.google.com" 279 | :notes [{:id 1}]} 280 | insert-item (partial edb/insert-item schema) 281 | db-with-items (-> db 282 | (insert-item :notes note) 283 | (insert-item :links link))] 284 | (let [note-from-db (edb/get-item-by-id schema db-with-items :notes 1) 285 | link-from-db (edb/get-item-by-id schema db-with-items :links 1) 286 | note-link (get ((:links note-from-db)) 0) 287 | note-link-note (get ((:notes note-link)) 0)] 288 | (= (:url note-link) "http://www.google.com") 289 | (= note-from-db note-link-note)))) 290 | 291 | (deftest nested-relations [] 292 | (let [db (-> {} 293 | (util/add-empty-layout :notes) 294 | (util/add-empty-layout :links) 295 | (util/add-empty-layout :users)) 296 | note {:id 1 297 | :title "Note title" 298 | :links [{:id 1}]} 299 | user {:id 1 :username "Retro"} 300 | link {:id 1 :url "http://google.com" :user {:id 1}} 301 | schema (assoc schema :links {:relations {:user [:one :users]}}) 302 | insert-item (partial edb/insert-item schema) 303 | db-with-items (-> db 304 | (insert-item :notes note) 305 | (insert-item :users user) 306 | (insert-item :links link)) 307 | link-from-relation (get ((:links (edb/get-item-by-id schema db-with-items :notes 1))) 0) 308 | user-from-relation ((:user link-from-relation))] 309 | (is (= user-from-relation user)))) 310 | 311 | (deftest tree-relations 312 | (let [schema {:notes {:relations {:notes [:many :notes]}}} 313 | db (util/add-empty-layout {} :notes) 314 | notes [{:id 1 :title "Note #1" :notes [{:id 2}]} 315 | {:id 2 :title "Note #2" :notes [{:id 3}]} 316 | {:id 3 :title "Note #3" :notes []}] 317 | insert-item (partial edb/insert-item schema) 318 | db-with-items (reduce (fn [db item] 319 | (insert-item db :notes item)) 320 | db notes) 321 | note-1 (edb/get-item-by-id schema db-with-items :notes 1)] 322 | (is (= (get-in ((:notes note-1)) [0 :title]) "Note #2")) 323 | (is (= (get-in ((:notes (get((:notes note-1)) 0))) [0 :title]) "Note #3")))) 324 | 325 | (deftest remove-collection [] 326 | (let [db {:notes {:store {1 {:id 1}} 327 | :c-one {:current 1 328 | :pinned 2} 329 | :c-many {:latest [1 2] 330 | :starred [2 3]}}} 331 | expected-db {:notes {:store {1 {:id 1}} 332 | :c-one {:pinned 2} 333 | :c-many {:starred [2 3]}}}] 334 | (is (= expected-db 335 | (-> db 336 | (edb/remove-named-item :notes :current) 337 | (edb/remove-collection :notes :latest)))))) 338 | 339 | (deftest empty-collection [] 340 | (let [db {:notes {:store {1 {:id 1} 2 {:id 2} 3 {:id 3}} 341 | :c-one {} 342 | :c-many {:latest [1 2] 343 | :starred [2 3] 344 | :test [1]}} 345 | :__meta-store__ {:c-one {} 346 | :c-many {} 347 | :store {[:notes :latest] {:foo :bar} 348 | [:notes :starred] {:bar :baz} 349 | [:notes :test] {:baz :qux}}}} 350 | expected-db {:notes 351 | {:c-one {} 352 | :store {1 {:id 1} 2 {:id 2} 3 {:id 3}} 353 | :c-many {:starred [] 354 | :latest [] 355 | :test []}} 356 | :__meta-store__ 357 | {:c-one {} 358 | :store {[:notes :latest] {:foo :bar} 359 | [:notes :test] {:baz :qux :foo :bar}} 360 | :c-many {}}}] 361 | (is (= expected-db 362 | (-> db 363 | (edb/empty-collection :notes :test {:foo :bar}) 364 | (edb/empty-collection :notes :starred nil) 365 | (edb/empty-collection :notes :latest)))))) 366 | 367 | (deftest item-meta [] 368 | (let [item-meta {:status :loaded} 369 | item {:id 1 :title "Note"} 370 | schema {:notes {}} 371 | db (util/add-empty-layout {} :notes) 372 | db-with-items (edb/insert-item schema db :notes item item-meta) 373 | expected-db {:notes {:store {1 item} 374 | :c-one {} 375 | :c-many {}} 376 | :__meta-store__ {:store {[:notes 1] item-meta} 377 | :c-one {} 378 | :c-many {}}}] 379 | (is (= db-with-items expected-db)) 380 | (is (= item-meta 381 | (meta (edb/get-item-by-id schema db-with-items :notes 1)))) 382 | (is (= item-meta (edb/get-item-meta schema db-with-items :notes 1))))) 383 | 384 | (deftest named-item-meta [] 385 | (let [collection-meta {:status :loading} 386 | item-meta {:status :loaded} 387 | item {:id 1 :title "Note"} 388 | schema {:notes {}} 389 | db (util/add-empty-layout {} :notes) 390 | 391 | db-with-collection 392 | (edb/insert-named-item schema db :notes :current-item nil collection-meta) 393 | 394 | db-with-collection-and-item 395 | (edb/insert-named-item schema db-with-collection :notes :current-item item item-meta) 396 | 397 | expected-db-with-collection 398 | {:notes {:store {} 399 | :c-one {:current-item nil} 400 | :c-many {}} 401 | :__meta-store__ {:store {[:notes :current-item] collection-meta} 402 | :c-one {} 403 | :c-many {}}} 404 | 405 | expected-db-with-collection-and-item 406 | {:notes {:store {1 item} 407 | :c-one {:current-item 1} 408 | :c-many {}} 409 | :__meta-store__ {:store {[:notes 1] item-meta} 410 | :c-one {} 411 | :c-many {}}}] 412 | (is (= db-with-collection expected-db-with-collection)) 413 | (is (= db-with-collection-and-item expected-db-with-collection-and-item)) 414 | (is (= collection-meta 415 | (edb/get-named-item-meta schema db-with-collection :notes :current-item))) 416 | (is (= item-meta 417 | (edb/get-named-item-meta schema db-with-collection-and-item :notes :current-item))))) 418 | 419 | (deftest collection-meta [] 420 | (let [collection-meta {:status :loading} 421 | items [{:id 1 :title "Note"}] 422 | schema {:notes {}} 423 | db (util/add-empty-layout {} :notes) 424 | 425 | db-with-collection 426 | (edb/insert-collection schema db :notes :latest items collection-meta) 427 | 428 | expected-db-with-collection 429 | {:notes {:store {1 (get items 0)} 430 | :c-one {} 431 | :c-many {:latest [1]}} 432 | :__meta-store__ {:store {[:notes :latest] collection-meta} 433 | :c-one {} 434 | :c-many {}}}] 435 | (is (= db-with-collection expected-db-with-collection)) 436 | (is (= collection-meta 437 | (edb/get-collection-meta schema db-with-collection :notes :latest))) 438 | (is (= collection-meta 439 | (meta (edb/get-collection schema db-with-collection :notes :latest)))))) 440 | 441 | (deftest removing-item-removes-its-id-from-collections [] 442 | (let [schema {:notes {}} 443 | insert-named-item (partial edb/insert-named-item schema) 444 | insert-collection (partial edb/insert-collection schema) 445 | note-1 {:id 1} 446 | note-2 {:id 2} 447 | db (util/add-empty-layout {} :notes) 448 | db-with-items-and-collections (-> db 449 | (insert-named-item :notes :current note-1) 450 | (insert-collection :notes :list [note-1 note-2])) 451 | expected-db-after-delete {:notes {:store {2 {:id 2}} 452 | :c-one {} 453 | :c-many {:list [2]}}}] 454 | (is (= expected-db-after-delete 455 | (edb/remove-item schema db-with-items-and-collections :notes 1))))) 456 | 457 | (deftest removing-item-removes-meta [] 458 | (let [db {:notes {:store {1 {:id 1}} 459 | :c-one {} 460 | :c-many {}} 461 | :__meta-store__ {:store {[:notes 1] {:status :loaded}} 462 | :c-one {} 463 | :c-many {}}} 464 | expected-db {:notes {:store {} 465 | :c-one {} 466 | :c-many {}} 467 | :__meta-store__ {:store {} 468 | :c-one {} 469 | :c-many {}}}] 470 | (is (= expected-db (edb/remove-item {} db :notes 1))))) 471 | 472 | (deftest removing-named-item-removes-meta [] 473 | (let [db {:notes {:store {} 474 | :c-one {:current-item nil} 475 | :c-many {}} 476 | :__meta-store__ {:store {[:notes :current-item] {:status :loaded}} 477 | :c-one {} 478 | :c-many {}}} 479 | expected-db {:notes {:store {} 480 | :c-one {} 481 | :c-many {}} 482 | :__meta-store__ {:store {} 483 | :c-one {} 484 | :c-many {}}}] 485 | (is (= expected-db (edb/remove-named-item db :notes :current-item))))) 486 | 487 | (deftest removing-collection-removes-meta [] 488 | (let [db {:notes {:store {} 489 | :c-one {} 490 | :c-many {:latest []}} 491 | :__meta-store__ {:store {[:notes :latest] {:status :loaded}} 492 | :c-one {} 493 | :c-many {}}} 494 | expected-db {:notes {:store {} 495 | :c-one {} 496 | :c-many {}} 497 | :__meta-store__ {:store {} 498 | :c-one {} 499 | :c-many {}}}] 500 | (is (= expected-db (edb/remove-collection db :notes :latest))))) 501 | 502 | (deftest vacuum [] 503 | (let [db {:notes {:store {1 {:id 1} 2 {:id 2} 3 {:id 3} 4 {:id 4}} 504 | :c-one {:current 1} 505 | :c-many {:latest [1 3]}} 506 | :__meta-store__ {:store {[:notes 2] {:status :loaded}} 507 | :c-one {} 508 | :c-many {}}} 509 | expected-db {:notes {:store {1 {:id 1} 3 {:id 3}} 510 | :c-one {:current 1} 511 | :c-many {:latest [1 3]}} 512 | :__meta-store__ {:store {} 513 | :c-one {} 514 | :c-many {}}}] 515 | (is (= (edb/vacuum db) expected-db)))) 516 | 517 | ;; DBAL 518 | (deftest dbal-insert-item [] 519 | (let [insert-item-fn (:insert-item dbal) 520 | db (insert-item-fn {} :notes {:id 1 :title "Note title"})] 521 | (is (= (get-in db [:notes :store 1 :title]) "Note title")))) 522 | 523 | (deftest inserting-item-with-relations-should-strip-them 524 | (let [schema {:user {:relations {:notes [:many :note]} 525 | :id :id} 526 | :note {:id :id}} 527 | user {:id 1 :notes [{:id 1} {:id 2}]} 528 | insert-named-item (partial edb/insert-named-item schema) 529 | insert-collection (partial edb/insert-collection schema) 530 | get-named-item (partial edb/get-named-item schema) 531 | db {} 532 | db-after-insert (insert-named-item db :user :current user) 533 | user-from-db (get-named-item db-after-insert :user :current)] 534 | (is (= {:user {:c-one {:current 1} 535 | :c-many {:list [1]} 536 | :store {1 {:id 1}}} 537 | :note {:store {1 {:id 1} 2 {:id 2}} 538 | :c-many {[:user 1 :notes] [1 2]}}} 539 | (insert-collection db-after-insert :user :list [user-from-db]))) 540 | (is (= {:user {:c-one {:current 1} 541 | :store {1 {:id 1}}} 542 | :note {:store {1 {:id 1} 2 {:id 2}} 543 | :c-many {[:user 1 :notes] [1 2]}}} 544 | (insert-named-item db-after-insert :user :current user-from-db) 545 | db-after-insert)))) 546 | 547 | (deftest getting-nil-item-with-relations-should-return-nil 548 | (let [schema {:user {:relations {:notes [:many :note]} 549 | :id :id} 550 | :note {:id :id}} 551 | get-named-item (partial edb/get-named-item schema) 552 | db {} ] 553 | (is (nil? (get-named-item db :user :current))))) 554 | 555 | 556 | #_(def schema {:notes {:id :id 557 | :middleware {:set [(fn [item] item)]} 558 | :relations {:user [:one :users] 559 | :links [:many :links]} 560 | :schema {}} 561 | :links {:id :id} 562 | :users {:id :id}}) 563 | 564 | (deftest get-relation-path 565 | (is (= (edb/get-relation-path schema :notes :user {:id 1}) 566 | [:notes 1 :user])) 567 | (is (= (edb/get-relation-path schema :notes :links {:id 1}) 568 | [:notes 1 :links]))) 569 | 570 | 571 | 572 | 573 | (deftest manipulating-relation-many [] 574 | (let [db (-> {} 575 | (util/add-empty-layout :notes) 576 | (util/add-empty-layout :links)) 577 | note {:id 1 :title "Note title" 578 | :links [{:id 1 :url "http://google.com"} 579 | {:id 2 :url "http://bing.com"}]} 580 | db-with-items (edb/insert-item schema db :notes note) 581 | expected-db {:notes {:store {1 {:id 1 :title "Note title"}} 582 | :c-one {} 583 | :c-many {}} 584 | :links {:store {1 {:id 1 :url "http://google.com"} 585 | 2 {:id 2 :url "http://bing.com"}} 586 | :c-one {} 587 | :c-many {[:notes 1 :links] [1 2]}}} 588 | expected-db-after-append {:notes {:store {1 {:id 1 :title "Note title"}} 589 | :c-one {} 590 | :c-many {}} 591 | :links {:store {1 {:id 1 :url "http://google.com"} 592 | 2 {:id 2 :url "http://bing.com"} 593 | 3 {:id 3 :url "http://altavista.com"}} 594 | :c-one {} 595 | :c-many {[:notes 1 :links] [1 2 3]}}} 596 | expected-db-after-prepend {:notes {:store {1 {:id 1 :title "Note title"}} 597 | :c-one {} 598 | :c-many {}} 599 | :links {:store {1 {:id 1 :url "http://google.com"} 600 | 2 {:id 2 :url "http://bing.com"} 601 | 4 {:id 4 :url "http://lycos.com" }} 602 | :c-one {} 603 | :c-many {[:notes 1 :links] [4 1 2]}}} 604 | expected-db-after-remove {:notes {:store {1 {:id 1 :title "Note title"}} 605 | :c-one {} 606 | :c-many {}} 607 | :links {:store {1 {:id 1 :url "http://google.com"} 608 | 2 {:id 2 :url "http://bing.com"} 609 | } 610 | :c-one {} 611 | :c-many {}}} 612 | expected-db-after-manual-insert {:notes {:store {1 {:id 1 :title "Note title"}} 613 | :c-one {} 614 | :c-many {}} 615 | :links {:store {1 {:id 1 :url "http://google.com"} 616 | 2 {:id 2 :url "http://bing.com"} 617 | 4 {:id 4 :url "http://lycos.com" }} 618 | :c-one {} 619 | :c-many {[:notes 1 :links] [4]}}}] 620 | (is (= db-with-items expected-db)) 621 | (is (= (edb/get-item-by-id schema db-with-items :links 1) {:id 1 :url "http://google.com"})) 622 | (is (= expected-db-after-append 623 | (edb/append-related-collection schema db-with-items :notes :links {:id 1} 624 | [{:id 3 :url "http://altavista.com"}]))) 625 | (is (= expected-db-after-prepend 626 | (edb/prepend-related-collection schema db-with-items :notes :links {:id 1} 627 | [{:id 4 :url "http://lycos.com"}]))) 628 | (is (= expected-db-after-remove 629 | (edb/remove-related-collection schema db-with-items :notes :links {:id 1}))) 630 | (is (= expected-db-after-manual-insert 631 | (edb/insert-related-collection schema db-with-items :notes :links {:id 1} [{:id 4 :url "http://lycos.com"}]))))) 632 | 633 | 634 | (deftest inserting-empty-collection-will-remove-existing-data 635 | (let [db (-> {} 636 | (util/add-empty-layout :notes)) 637 | db-with-items (edb/insert-collection schema db :notes :list [{:id 1} {:id 2}]) 638 | expected-db {:notes {:store {1 {:id 1} 2 {:id 2}} 639 | :c-many {:list [1 2]} 640 | :c-one {}}} 641 | expected-db-after-inserting-empty {:notes {:store {1 {:id 1} 2 {:id 2}} 642 | :c-many {} 643 | :c-one {}}}] 644 | (is (= db-with-items expected-db)) 645 | (is (= (edb/insert-collection schema db-with-items :notes :list []) 646 | expected-db-after-inserting-empty)))) 647 | 648 | (deftest inserting-empty-collection-to-relation-will-remove-existing-data 649 | (let [db (-> {} 650 | (util/add-empty-layout :notes) 651 | (util/add-empty-layout :links)) 652 | note {:id 1 :title "Note title" 653 | :links [{:id 1 :url "http://google.com"} 654 | {:id 2 :url "http://bing.com"}]} 655 | db-with-items (edb/insert-item schema db :notes note) 656 | expected-db {:notes {:store {1 {:id 1 :title "Note title"}} 657 | :c-one {} 658 | :c-many {}} 659 | :links {:store {1 {:id 1 :url "http://google.com"} 660 | 2 {:id 2 :url "http://bing.com"}} 661 | :c-one {} 662 | :c-many {[:notes 1 :links] [1 2]}}} 663 | expected-db-after-inserting-empty-relation 664 | {:notes {:store {1 {:id 1 :title "Note title"}} 665 | :c-one {} 666 | :c-many {}} 667 | :links {:store {1 {:id 1 :url "http://google.com"} 668 | 2 {:id 2 :url "http://bing.com"}} 669 | :c-one {} 670 | :c-many {}}}] 671 | (is (= db-with-items expected-db)) 672 | (is (= (edb/insert-item schema db-with-items :notes {:id 1 :links []}) 673 | expected-db-after-inserting-empty-relation)))) 674 | 675 | (deftest resolve-relations 676 | (let [db (-> {} 677 | (util/add-empty-layout :notes) 678 | (util/add-empty-layout :links) 679 | (util/add-empty-layout :users)) 680 | note {:id 1 681 | :title "Note title" 682 | :links [{:id 1}] 683 | :user {:id 1}} 684 | user {:id 1 :username "Retro"} 685 | link {:id 1 :url "http://google.com" :user {:id 1}} 686 | schema (-> schema 687 | (assoc-in [:notes :relations :user] [:one :users]) 688 | (assoc :links {:relations {:user [:one :users]}})) 689 | insert-item (partial edb/insert-item schema) 690 | db-with-items (-> db 691 | (insert-item :notes note) 692 | (insert-item :users user) 693 | (insert-item :links link))] 694 | (is (= {:id 1 695 | :title "Note title" 696 | :links [{:id 1 :url "http://google.com" :user {:id 1 :username "Retro"}}] 697 | :user {:id 1 :username "Retro"}} 698 | (edb/get-item-by-id schema db-with-items :notes 1 [:user [:links :user]]))))) 699 | 700 | (deftest resolve-relations-for-named-item 701 | (let [db (-> {} 702 | (util/add-empty-layout :notes) 703 | (util/add-empty-layout :links) 704 | (util/add-empty-layout :users)) 705 | note {:id 1 706 | :title "Note title" 707 | :links [{:id 1}] 708 | :user {:id 1}} 709 | user {:id 1 :username "Retro"} 710 | link {:id 1 :url "http://google.com" :user {:id 1}} 711 | schema (-> schema 712 | (assoc-in [:notes :relations :user] [:one :users]) 713 | (assoc :links {:relations {:user [:one :users]}})) 714 | insert-item (partial edb/insert-item schema) 715 | insert-named-item (partial edb/insert-named-item schema) 716 | db-with-items (-> db 717 | (insert-named-item :notes :current note) 718 | (insert-item :users user) 719 | (insert-item :links link))] 720 | (is (= {:id 1 721 | :title "Note title" 722 | :links [{:id 1 :url "http://google.com" :user {:id 1 :username "Retro"}}] 723 | :user {:id 1 :username "Retro"}} 724 | (edb/get-named-item schema db-with-items :notes :current false [:user [:links :user]]))))) 725 | 726 | 727 | (deftest resolve-relations-for-collection 728 | (let [db (-> {} 729 | (util/add-empty-layout :notes) 730 | (util/add-empty-layout :links) 731 | (util/add-empty-layout :users)) 732 | note {:id 1 733 | :title "Note title" 734 | :links [{:id 1}] 735 | :user {:id 1}} 736 | user {:id 1 :username "Retro"} 737 | link {:id 1 :url "http://google.com" :user {:id 1}} 738 | schema (-> schema 739 | (assoc-in [:notes :relations :user] [:one :users]) 740 | (assoc :links {:relations {:user [:one :users]}})) 741 | insert-item (partial edb/insert-item schema) 742 | insert-collection (partial edb/insert-collection schema) 743 | db-with-items (-> db 744 | (insert-collection :notes :list [note]) 745 | (insert-item :users user) 746 | (insert-item :links link))] 747 | (is (= [{:id 1 748 | :title "Note title" 749 | :links [{:id 1 :url "http://google.com" :user {:id 1 :username "Retro"}}] 750 | :user {:id 1 :username "Retro"}}] 751 | (edb/get-collection schema db-with-items :notes :list [:user [:links :user]]))))) 752 | -------------------------------------------------------------------------------- /test/entitydb/test.cljs: -------------------------------------------------------------------------------- 1 | (ns entitydb.test.test 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [cljs.test :as test] 4 | [entitydb.test.core])) 5 | 6 | (doo-tests 'entitydb.test.core) --------------------------------------------------------------------------------