├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── psql.sh └── setup-db.sh ├── build.clj ├── deps.edn ├── dev-resources ├── cgg-data.edn ├── logback-test.xml ├── my │ └── clojure_game_geek │ │ └── test_utils.clj └── user.clj ├── doc └── intro.md ├── docker-compose.yml ├── resources └── cgg-schema.edn ├── src └── my │ ├── clojure_game_geek.clj │ └── clojure_game_geek │ ├── db.clj │ ├── schema.clj │ ├── server.clj │ └── system.clj └── test └── my └── clojure_game_geek └── system_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea 13 | *.iml 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2022-12-16 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2022-12-16 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/my/clojure-game-geek/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/my/clojure-game-geek/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor to control, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojure-game-geek 2 | 3 | FIXME: my new application. 4 | 5 | ## Installation 6 | 7 | Download from https://github.com/my/clojure-game-geek 8 | 9 | ## Usage 10 | 11 | FIXME: explanation 12 | 13 | Run the project directly, via `:exec-fn`: 14 | 15 | $ clojure -X:run-x 16 | Hello, Clojure! 17 | 18 | Run the project, overriding the name to be greeted: 19 | 20 | $ clojure -X:run-x :name '"Someone"' 21 | Hello, Someone! 22 | 23 | Run the project directly, via `:main-opts` (`-m my.clojure-game-geek`): 24 | 25 | $ clojure -M:run-m 26 | Hello, World! 27 | 28 | Run the project, overriding the name to be greeted: 29 | 30 | $ clojure -M:run-m Via-Main 31 | Hello, Via-Main! 32 | 33 | Run the project's tests (they'll fail until you edit them): 34 | 35 | $ clojure -T:build test 36 | 37 | Run the project's CI pipeline and build an uberjar (this will fail until you edit the tests to pass): 38 | 39 | $ clojure -T:build ci 40 | 41 | This will produce an updated `pom.xml` file with synchronized dependencies inside the `META-INF` 42 | directory inside `target/classes` and the uberjar in `target`. You can update the version (and SCM tag) 43 | information in generated `pom.xml` by updating `build.clj`. 44 | 45 | If you don't want the `pom.xml` file in your project, you can remove it. The `ci` task will 46 | still generate a minimal `pom.xml` as part of the `uber` task, unless you remove `version` 47 | from `build.clj`. 48 | 49 | Run that uberjar: 50 | 51 | $ java -jar target/clojure-game-geek-0.1.0-SNAPSHOT.jar 52 | 53 | If you remove `version` from `build.clj`, the uberjar will become `target/clojure-game-geek-standalone.jar`. 54 | 55 | ## Options 56 | 57 | FIXME: listing of options this app accepts. 58 | 59 | ## Examples 60 | 61 | ... 62 | 63 | ### Bugs 64 | 65 | ... 66 | 67 | ### Any Other Sections 68 | ### That You Think 69 | ### Might be Useful 70 | 71 | ## License 72 | 73 | Copyright © 2022 Howard.lewisship 74 | 75 | _EPLv1.0 is just the default for projects generated by `clj-new`: you are not_ 76 | _required to open source this project, nor are you required to use EPLv1.0!_ 77 | _Feel free to remove or change the `LICENSE` file and remove or update this_ 78 | _section of the `README.md` file!_ 79 | 80 | Distributed under the Eclipse Public License version 1.0. 81 | -------------------------------------------------------------------------------- /bin/psql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker exec -ti --user postgres clojure-game-geek-db-1 psql -Ucgg_role cggdb 4 | -------------------------------------------------------------------------------- /bin/setup-db.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker exec -i --user postgres clojure-game-geek-db-1 createdb cggdb 4 | 5 | docker exec -i --user postgres clojure-game-geek-db-1 psql cggdb -a <<__END 6 | create user cgg_role password 'lacinia'; 7 | grant create on schema public to cgg_role; 8 | __END 9 | 10 | docker exec -i clojure-game-geek-db-1 psql -Ucgg_role cggdb -a <<__END 11 | drop table if exists designer_to_game; 12 | drop table if exists game_rating; 13 | drop table if exists member; 14 | drop table if exists board_game; 15 | drop table if exists designer; 16 | 17 | CREATE OR REPLACE FUNCTION mantain_updated_at() 18 | RETURNS TRIGGER AS \$\$ 19 | BEGIN 20 | NEW.updated_at = now(); 21 | RETURN NEW; 22 | END; 23 | \$\$ language 'plpgsql'; 24 | 25 | create table member ( 26 | member_id int generated by default as identity primary key, 27 | name text not null, 28 | created_at timestamp not null default current_timestamp, 29 | updated_at timestamp not null default current_timestamp); 30 | 31 | create trigger member_updated_at before update 32 | on member for each row execute procedure 33 | mantain_updated_at(); 34 | 35 | create table board_game ( 36 | game_id int generated by default as identity primary key, 37 | name text not null, 38 | summary text, 39 | min_players integer, 40 | max_players integer, 41 | created_at timestamp not null default current_timestamp, 42 | updated_at timestamp not null default current_timestamp); 43 | 44 | create trigger board_game_updated_at before update 45 | on board_game for each row execute procedure 46 | mantain_updated_at(); 47 | 48 | create table designer ( 49 | designer_id int generated by default as identity primary key, 50 | name text not null, 51 | uri text, 52 | created_at timestamp not null default current_timestamp, 53 | updated_at timestamp not null default current_timestamp); 54 | 55 | create trigger designer_updated_at before update 56 | on designer for each row execute procedure 57 | mantain_updated_at(); 58 | 59 | create table game_rating ( 60 | game_id int references board_game(game_id), 61 | member_id int references member(member_id), 62 | rating integer not null, 63 | created_at timestamp not null default current_timestamp, 64 | updated_at timestamp not null default current_timestamp, 65 | primary key (game_id, member_id)); 66 | 67 | create trigger game_rating_updated_at before update 68 | on game_rating for each row execute procedure 69 | mantain_updated_at(); 70 | 71 | create table designer_to_game ( 72 | designer_id int references designer(designer_id), 73 | game_id int references board_game(game_id), 74 | primary key (designer_id, game_id)); 75 | 76 | insert into board_game (game_id, name, summary, min_players, max_players) values 77 | (1234, 'Zertz', 'Two player abstract with forced moves and shrinking board', 2, 2), 78 | (1235, 'Dominion', 'Created the deck-building genre; zillions of expansions', 2, null), 79 | (1236, 'Tiny Epic Galaxies', 'Fast dice-based sci-fi space game with a bit of chaos', 1, 4), 80 | (1237, '7 Wonders: Duel', 'Tense, quick card game of developing civilizations', 2, 2); 81 | 82 | alter table board_game alter column game_id restart with 1300; 83 | 84 | insert into member (member_id, name) values 85 | (37, 'curiousattemptbunny'), 86 | (1410, 'bleedingedge'), 87 | (2812, 'missyo'); 88 | 89 | alter table member alter column member_id restart with 2900; 90 | 91 | insert into designer (designer_id, name, uri) values 92 | (200, 'Kris Burm', 'http://www.gipf.com/project_gipf/burm/burm.html'), 93 | (201, 'Antoine Bauza', 'http://www.antoinebauza.fr/'), 94 | (202, 'Bruno Cathala', 'http://www.brunocathala.com/'), 95 | (203, 'Scott Almes', null), 96 | (204, 'Donald X. Vaccarino', null); 97 | 98 | alter table designer alter column designer_id restart with 300; 99 | 100 | insert into designer_to_game (designer_id, game_id) values 101 | (200, 1234), 102 | (201, 1237), 103 | (204, 1235), 104 | (203, 1236), 105 | (202, 1237); 106 | 107 | insert into game_rating (game_id, member_id, rating) values 108 | (1234, 37, 3), 109 | (1234, 1410, 5), 110 | (1236, 1410, 4), 111 | (1237, 1410, 4), 112 | (1237, 2812, 4), 113 | (1237, 37, 5); 114 | __END 115 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:refer-clojure :exclude [test]) 3 | (:require [org.corfield.build :as bb])) 4 | 5 | (def lib 'net.clojars.my/clojure-game-geek) 6 | (def version "0.1.0-SNAPSHOT") 7 | (def main 'my.clojure-game-geek) 8 | 9 | (defn test "Run the tests." [opts] 10 | (bb/run-tests (assoc opts :aliases [:dev]))) 11 | 12 | (defn ci "Run the CI pipeline of tests (and build the uberjar)." [opts] 13 | (-> opts 14 | (assoc :lib lib :version version :main main) 15 | (bb/run-tests) 16 | (bb/clean) 17 | (bb/uber))) 18 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"} 3 | com.walmartlabs/lacinia {:mvn/version "1.2-alpha-4"} 4 | com.walmartlabs/lacinia-pedestal {:mvn/version "1.1"} 5 | org.clojure/java.jdbc {:mvn/version "0.7.12"} 6 | org.postgresql/postgresql {:mvn/version "42.5.1"} 7 | com.mchange/c3p0 {:mvn/version "0.9.5.5"} 8 | com.stuartsierra/component {:mvn/version "1.1.0"} 9 | io.aviso/logging {:mvn/version "1.0"}} 10 | :aliases 11 | {:run-m {:main-opts ["-m" "my.clojure-game-geek"]} 12 | :run-x {:ns-default my.clojure-game-geek 13 | :exec-fn greet 14 | :exec-args {:name "Clojure"}} 15 | :build {:deps {io.github.seancorfield/build-clj 16 | {:git/tag "v0.8.2" :git/sha "0ffdb4c" 17 | ;; since we're building an app uberjar, we do not 18 | ;; need deps-deploy for clojars.org deployment: 19 | :deps/root "slim"}} 20 | :ns-default build} 21 | :dev {:extra-paths ["dev-resources"]} 22 | :test {:extra-paths ["test"] 23 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} 24 | io.github.cognitect-labs/test-runner 25 | {:git/tag "v0.5.0" :git/sha "48c3c67"}}}}} 26 | -------------------------------------------------------------------------------- /dev-resources/cgg-data.edn: -------------------------------------------------------------------------------- 1 | {:games 2 | [{:id "1234" 3 | :name "Zertz" 4 | :summary "Two player abstract with forced moves and shrinking board" 5 | :designers #{"200"} 6 | :minPlayers 2 7 | :maxPlayers 2} 8 | {:id "1235" 9 | :name "Dominion" 10 | :summary "Created the deck-building genre; zillions of expansions" 11 | :designers #{"204"} 12 | :minPlayers 2} 13 | {:id "1236" 14 | :name "Tiny Epic Galaxies" 15 | :summary "Fast dice-based sci-fi space game with a bit of chaos" 16 | :designers #{"203"} 17 | :minPlayers 1 18 | :maxPlayers 4} 19 | {:id "1237" 20 | :name "7 Wonders: Duel" 21 | :summary "Tense, quick card game of developing civilizations" 22 | :designers #{"201" "202"} 23 | :minPlayers 2 24 | :maxPlayers 2}] 25 | 26 | :members 27 | [{:id "37" 28 | :name "curiousattemptbunny"} 29 | {:id "1410" 30 | :name "bleedingedge"} 31 | {:id "2812" 32 | :name "missyo"}] 33 | 34 | :ratings 35 | [{:member-id "37" :game-id "1234" :rating 3} 36 | {:member-id "1410" :game-id "1234" :rating 5} 37 | {:member-id "1410" :game-id "1236" :rating 4} 38 | {:member-id "1410" :game-id "1237" :rating 4} 39 | {:member-id "2812" :game-id "1237" :rating 4} 40 | {:member-id "37" :game-id "1237" :rating 5}] 41 | 42 | :designers 43 | [{:id "200" 44 | :name "Kris Burm" 45 | :url "http://www.gipf.com/project_gipf/burm/burm.html"} 46 | {:id "201" 47 | :name "Antoine Bauza" 48 | :url "http://www.antoinebauza.fr/"} 49 | {:id "202" 50 | :name "Bruno Cathala" 51 | :url "http://www.brunocathala.com/"} 52 | {:id "203" 53 | :name "Scott Almes"} 54 | {:id "204" 55 | :name "Donald X. Vaccarino"}]} 56 | -------------------------------------------------------------------------------- /dev-resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %-5level %logger - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dev-resources/my/clojure_game_geek/test_utils.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek.test-utils 2 | (:require [clojure.walk :as walk]) 3 | (:import (clojure.lang IPersistentMap))) 4 | 5 | (defn simplify 6 | "Converts all ordered maps nested within the map into standard hash maps, and 7 | sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems." 8 | [m] 9 | (walk/postwalk 10 | (fn [node] 11 | (cond 12 | (instance? IPersistentMap node) 13 | (into {} node) 14 | 15 | (seq? node) 16 | (vec node) 17 | 18 | :else 19 | node)) 20 | m)) 21 | -------------------------------------------------------------------------------- /dev-resources/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [com.stuartsierra.component :as component] 3 | [my.clojure-game-geek.db :as db] 4 | [my.clojure-game-geek.system :as system] 5 | [com.walmartlabs.lacinia :as lacinia] 6 | [clojure.java.browse :refer [browse-url]] 7 | [my.clojure-game-geek.test-utils :refer [simplify]])) 8 | 9 | (defonce system (system/new-system)) 10 | 11 | (defn q 12 | [query-string] 13 | (-> system 14 | :schema-provider 15 | :schema 16 | (lacinia/execute query-string nil nil) 17 | simplify)) 18 | 19 | (defn start 20 | [] 21 | (alter-var-root #'system component/start-system) 22 | (browse-url "http://localhost:8888/ide") 23 | :started) 24 | 25 | (defn stop 26 | [] 27 | (alter-var-root #'system component/stop-system) 28 | :stopped) 29 | 30 | (comment 31 | (start) 32 | (stop) 33 | 34 | (def db (:db system)) 35 | 36 | (require '[my.clojure-game-geek.db :as db]) 37 | 38 | (db/find-member-by-id db 37) 39 | (db/list-designers-for-game db 1237) 40 | (db/list-games-for-designer db 201) 41 | (db/list-ratings-for-game db 1234) 42 | (db/list-ratings-for-member db 1410) 43 | (db/upsert-game-rating db 1237 1410 3) 44 | (db/upsert-game-rating db 1234 2812 4) 45 | 46 | ) 47 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to clojure-game-geek 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | ports: 5 | - 25432:5432 6 | image: postgres:15.1-alpine 7 | environment: 8 | POSTGRES_PASSWORD: supersecret 9 | -------------------------------------------------------------------------------- /resources/cgg-schema.edn: -------------------------------------------------------------------------------- 1 | {:objects 2 | {:BoardGame 3 | {:description "A physical or virtual board game." 4 | :fields 5 | {:id {:type (non-null Int)} 6 | :name {:type (non-null String)} 7 | :summary {:type String 8 | :description "A one-line summary of the game."} 9 | :ratingSummary {:type (non-null :GameRatingSummary) 10 | :description "Summarizes member ratings for the game."} 11 | :description {:type String 12 | :description "A long-form description of the game."} 13 | :designers {:type (non-null (list :Designer)) 14 | :description "Designers who contributed to the game."} 15 | :minPlayers {:type Int 16 | :description "The minimum number of players the game supports."} 17 | :maxPlayers {:type Int 18 | :description "The maximum number of players the game supports."} 19 | :playTime {:type Int 20 | :description "Play time, in minutes, for a typical game."}}} 21 | 22 | :GameRatingSummary 23 | {:description "Summary of ratings for a single game." 24 | :fields 25 | {:count {:type (non-null Int) 26 | :description "Number of ratings provided for the game. Ratings are 1 to 5 stars."} 27 | :average {:type (non-null Float) 28 | :description "The average value of all ratings, or 0 if never rated."}}} 29 | 30 | :Member 31 | {:description "A member of Clojure Game Geek. Members can rate games." 32 | :fields 33 | {:id {:type (non-null Int)} 34 | :name {:type (non-null String) 35 | :description "Unique name of the member."} 36 | :ratings {:type (list :GameRating) 37 | :description "List of games and ratings provided by this member."}}} 38 | 39 | :GameRating 40 | {:description "A member's rating of a particular game." 41 | :fields 42 | {:game {:type (non-null :BoardGame) 43 | :description "The Game rated by the member."} 44 | :rating {:type (non-null Int) 45 | :description "The rating as 1 to 5 stars."}}} 46 | 47 | :Designer 48 | {:description "A person who may have contributed to a board game design." 49 | :fields 50 | {:id {:type (non-null Int)} 51 | :name {:type (non-null String)} 52 | :url {:type String 53 | :description "Home page URL, if known."} 54 | :games {:type (non-null (list :BoardGame)) 55 | :description "Games designed by this designer."}}} 56 | 57 | :Query 58 | {:fields 59 | {:gameById 60 | {:type :BoardGame 61 | :description "Access a BoardGame by its unique id, if it exists." 62 | :args 63 | {:id {:type Int}}} 64 | 65 | :memberById 66 | {:type :Member 67 | :description "Access a ClojureGameGeek Member by their unique id, if it exists." 68 | :args 69 | {:id {:type (non-null Int)}}}}} 70 | 71 | :Mutation 72 | {:fields 73 | {:rateGame 74 | {:type :BoardGame 75 | :description "Establishes a rating of a board game, by a Member." 76 | :args 77 | {:gameId {:type (non-null Int)} 78 | :memberId {:type (non-null Int)} 79 | :rating {:type (non-null Int) 80 | :description "Game rating as number between 1 and 5."}}}}}}} 81 | -------------------------------------------------------------------------------- /src/my/clojure_game_geek.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek 2 | (:gen-class)) 3 | 4 | (defn greet 5 | "Callable entry point to the application." 6 | [data] 7 | (println (str "Hello, " (or (:name data) "World") "!"))) 8 | 9 | (defn -main 10 | "I don't do a whole lot ... yet." 11 | [& args] 12 | (greet {:name (first args)})) 13 | -------------------------------------------------------------------------------- /src/my/clojure_game_geek/db.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek.db 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [io.pedestal.log :as log] 4 | [clojure.string :as string] 5 | [clojure.set :as set] 6 | [com.stuartsierra.component :as component]) 7 | (:import (com.mchange.v2.c3p0 ComboPooledDataSource))) 8 | 9 | (defn- pooled-data-source 10 | [host dbname user password port] 11 | (doto (ComboPooledDataSource.) 12 | (.setDriverClass "org.postgresql.Driver") 13 | (.setJdbcUrl (str "jdbc:postgresql://" host ":" port "/" dbname)) 14 | (.setUser user) 15 | (.setPassword password))) 16 | 17 | (defrecord ClojureGameGeekDb [^ComboPooledDataSource datasource] 18 | 19 | component/Lifecycle 20 | 21 | (start [this] 22 | (assoc this :datasource (pooled-data-source "localhost" "cggdb" "cgg_role" "lacinia" 25432))) 23 | 24 | (stop [this] 25 | (.close datasource) 26 | (assoc this :datasource nil))) 27 | 28 | (defn- query 29 | [component statement] 30 | (let [[sql & params] statement] 31 | (log/debug :sql (string/replace sql #"\s+" " ") 32 | :params params)) 33 | (jdbc/query component statement)) 34 | 35 | (defn- execute! 36 | [component statement] 37 | (let [[sql & params] statement] 38 | (log/debug :sql (string/replace sql #"\s+" " ") 39 | :params params)) 40 | (jdbc/execute! component statement)) 41 | 42 | (defn- remap-board-game 43 | [row-data] 44 | (set/rename-keys row-data {:game_id :id 45 | :min_players :minPlayers 46 | :max_players :maxPlayers 47 | :created_at :createdAt 48 | :updated_at :updatedAt})) 49 | 50 | (defn- remap-member 51 | [row-data] 52 | (set/rename-keys row-data {:member_id :id 53 | :created_at :createdAt 54 | :updated_at :updatedAt})) 55 | 56 | (defn- remap-designer 57 | [row-data] 58 | (set/rename-keys row-data {:designer_id :id 59 | :created_at :createdAt 60 | :updated_at :updatedAt})) 61 | 62 | (defn- remap-rating 63 | [row-data] 64 | (set/rename-keys row-data {:member_id :member-id 65 | :game_id :game-id 66 | :created_at :createdAt 67 | :updated_at :updatedAt})) 68 | 69 | (defn find-game-by-id 70 | [component game-id] 71 | (-> (query component 72 | ["select game_id, name, summary, min_players, max_players, created_at, updated_at 73 | from board_game where game_id = ?" game-id]) 74 | first 75 | remap-board-game)) 76 | 77 | (defn find-member-by-id 78 | [component member-id] 79 | (-> (query component 80 | ["select member_id, name, created_at, updated_at 81 | from member 82 | where member_id = ?" member-id]) 83 | first 84 | remap-member)) 85 | 86 | (defn list-designers-for-game 87 | [component game-id] 88 | (->> (query component 89 | ["select d.designer_id, d.name, d.uri, d.created_at, d.updated_at 90 | from designer d 91 | inner join designer_to_game j on (d.designer_id = j.designer_id) 92 | where j.game_id = ? 93 | order by d.name" game-id]) 94 | (map remap-designer))) 95 | 96 | (defn list-games-for-designer 97 | [component designer-id] 98 | (->> (query component 99 | ["select g.game_id, g.name, g.summary, g.min_players, g.max_players, g.created_at, 100 | g.updated_at 101 | from board_game g 102 | inner join designer_to_game j on (g.game_id = j.game_id) 103 | where j.designer_id = ? 104 | order by g.name" designer-id]) 105 | (map remap-board-game))) 106 | 107 | (defn list-ratings-for-game 108 | [component game-id] 109 | (->> (query component 110 | ["select game_id, member_id, rating, created_at, updated_at 111 | from game_rating 112 | where game_id = ?" game-id]) 113 | (map remap-rating))) 114 | 115 | (defn list-ratings-for-member 116 | [component member-id] 117 | (->> (query component 118 | ["select game_id, member_id, rating, created_at, updated_at 119 | from game_rating 120 | where member_id = ?" member-id]) 121 | (map remap-rating))) 122 | 123 | (defn upsert-game-rating 124 | "Adds a new game rating, or changes the value of an existing game rating. 125 | 126 | Returns nil." 127 | [component game-id member-id rating] 128 | (execute! component 129 | ["insert into game_rating (game_id, member_id, rating) 130 | values (?, ?, ?) 131 | on conflict (game_id, member_id) do update set rating = ?" 132 | game-id member-id rating rating]) 133 | nil) 134 | -------------------------------------------------------------------------------- /src/my/clojure_game_geek/schema.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek.schema 2 | "Contains custom resolvers and a function to provide the full schema." 3 | (:require [clojure.java.io :as io] 4 | [com.stuartsierra.component :as component] 5 | [com.walmartlabs.lacinia.util :as util] 6 | [com.walmartlabs.lacinia.schema :as schema] 7 | [com.walmartlabs.lacinia.resolve :refer [resolve-as]] 8 | [my.clojure-game-geek.db :as db] 9 | [clojure.edn :as edn])) 10 | 11 | (defn game-by-id 12 | [db] 13 | (fn [_ args _] 14 | (db/find-game-by-id db (:id args)))) 15 | 16 | (defn member-by-id 17 | [db] 18 | (fn [_ args _] 19 | (db/find-member-by-id db (:id args)))) 20 | 21 | (defn board-game-designers 22 | [db] 23 | (fn [_ _ board-game] 24 | (db/list-designers-for-game db (:id board-game)))) 25 | 26 | (defn designer-games 27 | [db] 28 | (fn [_ _ designer] 29 | (db/list-games-for-designer db (:id designer)))) 30 | 31 | (defn rating-summary 32 | [db] 33 | (fn [_ _ board-game] 34 | (let [ratings (map :rating (db/list-ratings-for-game db (:id board-game))) 35 | n (count ratings)] 36 | {:count n 37 | :average (if (zero? n) 38 | 0 39 | (/ (apply + ratings) 40 | (float n)))}))) 41 | 42 | (defn member-ratings 43 | [db] 44 | (fn [_ _ member] 45 | (db/list-ratings-for-member db (:id member)))) 46 | 47 | (defn game-rating->game 48 | [db] 49 | (fn [_ _ game-rating] 50 | (db/find-game-by-id db (:game-id game-rating)))) 51 | 52 | (defn rate-game 53 | [db] 54 | (fn [_ args _] 55 | (let [{game-id :gameId 56 | member-id :memberId 57 | rating :rating} args 58 | game (db/find-game-by-id db game-id) 59 | member (db/find-member-by-id db member-id)] 60 | (cond 61 | (nil? game) 62 | (resolve-as nil {:message "Game not found" 63 | :status 404}) 64 | 65 | (nil? member) 66 | (resolve-as nil {:message "Member not found" 67 | :status 404}) 68 | 69 | (not (<= 1 rating 5)) 70 | (resolve-as nil {:message "Rating must be between 1 and 5" 71 | :status 400}) 72 | 73 | :else 74 | (do 75 | (db/upsert-game-rating db game-id member-id rating) 76 | game))))) 77 | 78 | (defn resolver-map 79 | [component] 80 | (let [{:keys [db]} component] 81 | {:Query/gameById (game-by-id db) 82 | :Query/memberById (member-by-id db) 83 | :Mutation/rateGame (rate-game db) 84 | :BoardGame/designers (board-game-designers db) 85 | :BoardGame/ratingSummary (rating-summary db) 86 | :Designer/games (designer-games db) 87 | :Member/ratings (member-ratings db) 88 | :GameRating/game (game-rating->game db)})) 89 | 90 | (defn load-schema 91 | [component] 92 | (-> (io/resource "cgg-schema.edn") 93 | slurp 94 | edn/read-string 95 | (util/inject-resolvers (resolver-map component)) 96 | schema/compile)) 97 | 98 | (defrecord SchemaProvider [db schema] 99 | 100 | component/Lifecycle 101 | 102 | (start [this] 103 | (assoc this :schema (load-schema this))) 104 | 105 | (stop [this] 106 | (assoc this :schema nil))) 107 | -------------------------------------------------------------------------------- /src/my/clojure_game_geek/server.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek.server 2 | (:require [com.stuartsierra.component :as component] 3 | [com.walmartlabs.lacinia.pedestal2 :as lp] 4 | [io.pedestal.http :as http])) 5 | 6 | (defrecord Server [schema-provider server port] 7 | 8 | component/Lifecycle 9 | 10 | (start [this] 11 | (assoc this :server (-> schema-provider 12 | :schema 13 | (lp/default-service {:port port}) 14 | http/create-server 15 | http/start))) 16 | 17 | (stop [this] 18 | (http/stop server) 19 | (assoc this :server nil))) 20 | -------------------------------------------------------------------------------- /src/my/clojure_game_geek/system.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek.system 2 | (:require [com.stuartsierra.component :as component] 3 | [my.clojure-game-geek.schema :as schema] 4 | [my.clojure-game-geek.server :as server] 5 | [my.clojure-game-geek.db :as db])) 6 | 7 | (defn new-system 8 | ([] 9 | (new-system nil)) 10 | ([opts] 11 | (let [{:keys [port] 12 | :or {port 8888}} opts] 13 | (assoc (component/system-map) 14 | :db (db/map->ClojureGameGeekDb {}) 15 | :server (component/using (server/map->Server {:port port}) 16 | [:schema-provider]) 17 | :schema-provider (component/using 18 | (schema/map->SchemaProvider {}) 19 | [:db]))))) 20 | -------------------------------------------------------------------------------- /test/my/clojure_game_geek/system_test.clj: -------------------------------------------------------------------------------- 1 | (ns my.clojure-game-geek.system-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [com.stuartsierra.component :as component] 4 | [com.walmartlabs.lacinia :as lacinia] 5 | [my.clojure-game-geek.test-utils :refer [simplify]] 6 | [my.clojure-game-geek.system :as system])) 7 | 8 | (defn- test-system 9 | "Creates a new system suitable for testing, and ensures that 10 | the HTTP port won't conflict with a default running system." 11 | [] 12 | (system/new-system {:port 8989})) 13 | 14 | (defn- q 15 | "Extracts the compiled schema and executes a query." 16 | [system query variables] 17 | (-> system 18 | (get-in [:schema-provider :schema]) 19 | (lacinia/execute query variables nil) 20 | simplify)) 21 | 22 | (deftest can-read-board-game 23 | (let [system (component/start-system (test-system))] 24 | (try 25 | (is (= {:data {:gameById {:name "Zertz" 26 | :summary "Two player abstract with forced moves and shrinking board" 27 | :maxPlayers 2 28 | :minPlayers 2 29 | :playTime nil}}} 30 | (q system 31 | "{ gameById(id: 1234) { name summary minPlayers maxPlayers playTime }}" 32 | nil))) 33 | (finally 34 | (component/stop-system system))))) 35 | --------------------------------------------------------------------------------