├── .gitignore ├── .travis.yml ├── FAQ.md ├── README.md ├── client └── index.html ├── install.md ├── package.json ├── query.sql ├── schema.sql ├── server ├── bot.js ├── db.js ├── lanip.js ├── request_handlers.js ├── server.js └── utils.js ├── test ├── bot.test.js ├── db.test.js ├── fixtures │ ├── followers.json │ ├── following.json │ ├── make-fixture.js │ ├── members.json │ ├── org.json │ ├── person.json │ ├── repo.json │ └── stargazers.json ├── server.test.js └── utils.test.js └── tutorial.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (https://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | package-lock.json 35 | .nyc_output/ 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | env: 5 | global: 6 | - DATABASE_URL=postgres://postgres:@localhost/codeface 7 | - NODE_ENV=TEST 8 | services: 9 | - postgresql 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | 2 | ## Why PostgreSQL and _Not_ MySQL? 3 | 4 | The _good_ news is that almost all of your PostgreSQL knowledge 5 | is _directly_ transferable to [MySQL](https://en.wikipedia.org/wiki/MySQL). 6 | Since _both_ use SQL as the language 7 | for interacting with the database, 8 | the time you invest in learning PostgreSQL 9 | and building SQL skills is a hugely valuable. 10 | 11 | Learning how to _run_ means you also know how to _walk_. 12 | PostgreSQL might _feel_ "more difficult" 13 | in the same way that , but the principals are all the same. 14 | Just stick with it and keep asking questions until it all "makes sense". 15 | If you need to _apply_ your SQL skills to MySQL, MS SQL or MariaDB, 16 | it will only take you a few minutes to adapt to it. 17 | 18 | It's very much like riding a bicycle. 19 | Once you know how to balance, pedal and steer, 20 | your skills transfer to other bicycles. 21 | 22 | 23 | 24 | The _reason_ MySQL is still _hugely_ popular 25 | can be summarised by _one_ word: 26 | [***WordPress***](https://en.wikipedia.org/wiki/WordPress). 27 | 28 | Over **30%** of the 10 million most popular websites use WordPress. 29 | WordPress runs on the "LAMP" (_Linux Apache **MySQL** PHP_) stack, 30 | which means that people are using MySQL by _`default`_ 31 | not _conscious enlightenment_. 32 | + https://www.whoishostingthis.com/compare/wordpress/stats 33 | + https://w3techs.com/technologies/overview/content_management/all 34 | 35 | 36 | ## Why _Not_ Use WordPress? 37 | 38 | WordPress is _unquestionably_ a good CMS and blogging platform 39 | that helps millions of people/businesses publish online. 40 | Sadly, it's not secure by _default_ and when a vulnerability is discovered, 41 | it gets exploited en-mass very quickly. 42 | Yes, WordPress can be 43 | ["Hardened"](https://codex.wordpress.org/Hardening_WordPress) 44 | but that is _usually_ not the _first_ thing on people's todo list 45 | when launching a website or blog. 46 | The result is that _thousands_ of WordPress websites get hacked 47 | each time a patch is released e.g: 48 | https://www.zdnet.com/article/thousands-of-wordpress-sites-backdoored-with-malicious-code 49 | and it creates a maintenance headache 50 | for the person/people _responsible_ for the site. 51 | We're not saying you (_or anyone else_) should not use WordPress, 52 | just make sure you follow the the latest "best practice" if you do. 53 | (_We have been "burned" by it through no fault of our own... 54 | and would not touch it again with a barge pole! 55 | There are **much** more **secure** and **performant** options!_) 56 | 57 | ### What About NoSQL Databases/Datastores Like ElasticSearch and Redis? 58 | 59 | @dwyl we are _huge_ fans of _special-purpose_ data storage/retrieval systems. 60 | We have used _several_ NoSQL databases including CouchDB, ElasticSearch, 61 | MongoDB, Neo4J and Redis. 62 | Of these we _recommend_ ElasticSearch for full-text search 63 | and Redis for in-momory datasets and caching. see: 64 | 65 | + [github.com/dwyl/learn-**elasticsearch**](https://github.com/dwyl/learn-elasticsearch) 66 | + [github.com/dwyl/learn-**redis**](https://github.com/dwyl/learn-redis) 67 | 68 | However as a "primary" datastore with a robust query language, 69 | we feel PostgreSQL is the _clear_ winner as a "first" database. 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Learn PostgreSQL 4 | 5 | Learn how to use PostgreSQL 6 | and Structured Query Language (SQL) to store 7 | and query your data. 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | [![Build Status](https://img.shields.io/travis/dwyl/learn-postgresql/master.svg?style=flat-square)](https://travis-ci.org/dwyl/learn-postgresql) 18 | [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/learn-postgresql/master.svg?style=flat-square)](https://codecov.io/github/dwyl/learn-postgresql?branch=master) 19 | [![Dependencies: None!](https://david-dm.org/dwyl/learn-postgresql/status.svg?style=flat-square)](https://david-dm.org/dwyl/learn-postgresql) 20 | [![devDependencies Status](https://david-dm.org/dwyl/learn-postgresql/dev-status.svg?style=flat-square)](https://david-dm.org/dwyl/learn-postgresql?type=dev) 21 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/learn-postgresql/issues) 22 | 25 | 26 |
27 | 28 | 29 | # _Why_? 30 | 31 | Helping people store, retrieve and derive insights from data 32 | is the essence of _all_ software applications.
33 | 34 | ## SQL is _Everywhere_ 35 | 36 | Like it or not, Relational Databases store 37 | _most_ of the world's structured data 38 | and Structured Query Language (SQL) 39 | is _by far_ the most frequent way of retrieving the data.
40 | 41 | According to the most _recent_ surveys/statistics, 42 | SQL _still_ dominates the world of databases. 43 | 44 | https://insights.stackoverflow.com/survey/2018/#technology-databases 45 | ![stackoverflow-survey-2018-databases](https://user-images.githubusercontent.com/194400/52594468-80cb3b80-2e43-11e9-867a-eeb4eea9a322.png) 46 | 47 | 48 | https://db-engines.com/en/ranking 49 | ![dbms-ranking](https://user-images.githubusercontent.com/194400/52594416-64c79a00-2e43-11e9-8a61-02af22554802.png) 50 | 51 | > _**Note**: you should never adopt a technology 52 | based on it's **current popularity**, 53 | also be ware of_ 54 | ["_argumentum ad populum_"](https://en.wikipedia.org/wiki/Argumentum_ad_populum) 55 | ("_it's popular therefore you should use it_"). 56 | _Always pick the **appropriate tool** for the job 57 | based on the requirements, constraints and/or availability 58 | (both of "skill" on your existing team or in the wider community). 59 | We include these stats to explain that **relational databases** 60 | are **still** the most widely used **by far** and so 61 | learning SQL skills is a very **wise investment** 62 | both as an **individual** and for your **team** or **organisation**._ 63 | 64 | 65 | ## PostgreSQL is _Easy_ to Learn and it Runs _Everywhere_! 66 | 67 | Getting started with PostgreSQL is _easy_, 68 | (_just follow the steps in this guide and try out the example queries!_)
69 | When you are ready to _deploy_ your app, you are in safe hands, 70 | PostgreSQL runs _everywhere_: 71 | 72 | + **Travis-CI** (free) Integration Testing: 73 | https://docs.travis-ci.com/user/database-setup/#postgresql 74 | + **Heroku** PostgreSQL (_free for MVP: 10k rows_): https://www.heroku.com/postgres 75 | + AWS RDS Postgres (_good value + high performance_): 76 | https://aws.amazon.com/rds/postgresql/ 77 | + Google Cloud SQL: https://cloud.google.com/sql/ 78 | + DigitalOcean: https://www.digitalocean.com/products/managed-databases/ 79 | + Linode: 80 | https://www.linode.com/docs/databases/postgresql/create-a-highly-available-postgresql-cluster-using-patroni-and-haproxy/ 81 | + Azure: https://azure.microsoft.com/en-us/services/postgresql/ 82 | + Citus: https://techcrunch.com/2019/01/24/microsoft-acquires-citus-data 83 | + Self-managed high availability cluster: https://github.com/sorintlab/stolon 84 | 85 | # _Who_? 86 | 87 | _Everyone_ building _any_ application that stores data should learn SQL. 88 | SQL is _ubiquitous_ in every field/industry and the sooner you learn/master it, 89 | the higher your life-time return on time investment. 90 | 91 | Learning how to use a relational database is a foundational skill 92 | for all of computer science and application development. 93 | 94 | Being _proficient_ in SQL will open the door to Data Science with 95 | [SQL-on-Hadoop](https://mapr.com/why-hadoop/sql-hadoop/sql-hadoop-details/) 96 | [Apache Spark](https://en.wikipedia.org/wiki/Apache_Spark#Spark_SQL), 97 | Google [BigQuery](https://en.wikipedia.org/wiki/BigQuery), 98 | [Oracle](https://en.wikipedia.org/wiki/Oracle_Corporation#Controversies) 99 | and [Teradata](https://en.wikipedia.org/wiki/Teradata). 100 | In short, get _really_ good at SQL! It's _very_ useful. 101 | 102 | # _What_? 103 | 104 | This tutorial covers 5 areas: 105 | 106 | 1. _What_ is PostgreSQL? 107 | 2. _How_ do I get _started_ with PostgreSQL? (_a fully functioning example!_) 108 | 3. What is Structured Query Language (SQL)? (_lots of example queries!_) 109 | 4. _How_ do I write my _own_ SQL Queries? 110 | 5. _How_ do I deploy my own PostgreSQL-based Application? 111 | 112 | Once you have covered these areas, 113 | you will _know_ if PostgreSQL 114 | is "right" for your needs, 115 | or if you need to keep looking for a different way 116 | to store data. 117 | 118 | Let's dive in! 119 | 120 | ## 1. What is PostgreSQL? 121 | 122 | PostgreSQL (_often shortened to simply "**Postgres**"_) 123 | is an advanced 124 | **R**elational **D**ataBase **M**anagement **S**ystem ("RDBMS"), 125 | that lets you _efficiently_ and _securely_ store _any_ type of data. 126 | We will _explain_ "Relational Database" in the context 127 | of our _example_ below, 128 | so don't worry if it sounds like a buzzword soup. 129 | 130 | Postgres has an emphasis on standards compliance and extensibility 131 | which means there are many plugins you can use to enhance it 132 | like [PostGIS](https://postgis.net) for mapping applications 133 | and entire projects built on top of it like 134 | [TimescaleDB](https://www.timescale.com/) 135 | (_a time-series database perfect for analytics_) 136 | and [AgensGraph](https://bitnine.net/) 137 | (_a graph database, great for modelling networks e.g a "social graph"_). 138 | 139 | 140 | Structured Query Language (SQL) 141 | is the preferred means of interacting with data at any scale.
142 | 143 | > The _only_ reason MySQL is still more widely used than Postgres 144 | can be summarised in *one word*: **WordPress**. 145 | > WordPress has a firm grip on the CMS-based website market 146 | and it shows no sign of slowing down. 147 | > If your goal is to build CMS-based websites, 148 | or the company you _already_ work for uses 149 | [WordPress](https://www.cvedetails.com/vendor/2337/Wordpress.html), 150 | you should go for it! 151 | > If you prefer a more _general_ introduction to SQL, 152 | > follow _this_ tutorial! 153 | > The knowledge you will gain by learning Postgres is 95%+ 154 | "_transferable_" to other SQL databases so don't worry about 155 | the differences between MySQL and Postgres for now. 156 | If you're curious, read: https://hackr.io/blog/postgresql-vs-mysql 157 | 158 | 159 | 202 | 203 | # _How_? 204 | 205 | ### Installation 206 | 207 | Before you get started with using PostgreSQL, you'll have to install it. 208 | Follow these steps to get started: 209 | 210 | #### MacOS 211 | 212 | 1. There are a couple of ways to install PostgreSQL. One of the easier ways to 213 | get started is with Postgres.app. Navigate to https://postgresapp.com/ and then 214 | click "Download": 215 | ![download](https://cloud.githubusercontent.com/assets/12450298/19641848/6d3cfa4a-99da-11e6-858f-3ff2ada026be.png) 216 | 217 | 2. Once it's finished downloading, double click on the file to unzip then move 218 | the PostgreSQL elephant icon into your `applications` folder. Double click the 219 | icon to launch the application. 220 | 221 | 3. You should now see a new window launched with a list of servers to the left side of the window 222 | (if it's a fresh install, you should see one named `PostgreSQL XX`). 223 | If it shows anything else or an error props up, make sure you don't have any other instances of Postgres on your computer and reinstall. 224 | To fully reinstall follow [these steps](https://postgresapp.com/documentation/install.html) to delete data directories and preferences. 225 | Click on the button 'Initialize' (or 'Start' if you had already installed previously). 226 | download 227 | 228 | 4. Run `sudo mkdir -p /etc/paths.d && echo /Applications/Postgres.app/Contents/Versions/latest/bin | sudo tee /etc/paths.d/postgresapp` 229 | (found [here](https://postgresapp.com/documentation/install.html)) to use `psql` in the terminal. 230 | Close and open the terminal. 231 | 232 | 5. Postgres.app will by default create a role and database that matches your current macOS username. You can connect straight away by running `psql`. 233 | 234 | 6. You should then see something in your terminal that looks like this (with your macOS username in front of the prompt rather than 'postgres'): 235 | 236 | ![terminal](https://cloud.githubusercontent.com/assets/12450298/19642816/f8ac0c66-99de-11e6-87e2-db55e6abc27b.png) 237 | 238 | 7. You should now be all set up to start using PostgreSQL. For documentation on 239 | command line tools etc see https://postgresapp.com/documentation/ 240 | 241 | #### Ubuntu 242 | 243 | Digital Ocean have got a great article on [getting started with postgres]( https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-16-04). A quick summary is below. 244 | 245 | ##### Installation 246 | 247 | ``` 248 | sudo apt-get update 249 | sudo apt-get install postgresql postgresql-contrib 250 | ``` 251 | 252 | By default the only role created is the default 'postgres', so PostgreSQL will only respond to connections from an Ubuntu user called 'postgres'. We need to pretend to be that user and create a role matching our actual Ubuntu username: 253 | 254 | ``` 255 | sudo -u postgres createuser --interactive 256 | ``` 257 | 258 | This command means 'run the command `createuser --interactive` as the user called "postgres"'. 259 | 260 | When asked for the name of the role enter your Ubuntu username. If you're not sure, open a new Terminal tab and run `whoami`. 261 | 262 | When asked if you want to make the role a superuser, type 'y'. 263 | 264 | We now need to create the database matching the role name, as PostgreSQL expects this. Run: 265 | 266 | ``` 267 | sudo -u postgres createdb [your user name] 268 | ``` 269 | 270 | You can now connect to PostgreSQL by running `psql`. 271 | 272 | ### Create your first PostgreSQL database 273 | 274 | 1. To start PostgreSQL, type this command into the terminal: 275 | `psql` 276 | 277 | 2. Next type this command into the PostgreSQL interface: 278 | `CREATE DATABASE test;` 279 | **NOTE:** Don't forget the semi-colon. If you do, useful error messages won't 280 | show up. 281 | 282 | 3. To check that our database has been created, type `\l` into the psql prompt. 283 | You should see something like this in your terminal: 284 | ![test db](https://cloud.githubusercontent.com/assets/12450298/19650613/ce278678-9a01-11e6-89ad-b124c0adcfe5.png) 285 | 286 | ### Create new users for your database 287 | 288 | 1. If you closed the PostgreSQL server, start it again with: 289 | ` psql` 290 | 291 | 2. To create a new user, type the following into the psql prompt: 292 | ```sql 293 | CREATE USER testuser; 294 | ``` 295 | 296 | 3. Check that your user has been created. Type `\du` into the prompt. You should 297 | see something like this: 298 | ![user](https://cloud.githubusercontent.com/assets/12450298/19650852/9c340708-9a02-11e6-8f06-75f1e30a86b3.png) 299 | Users can be given certain permissions to access any given database you have 300 | created. 301 | 302 | 4. Next we need to give our user permissions to access the test database we 303 | created above. Enter the following command into the `psql` prompt: 304 | ```sql 305 | GRANT ALL PRIVILEGES ON DATABASE test TO testuser; 306 | ``` 307 | 308 | 309 | ### PostGIS - Spacial and Geographic objects for PostgreSQL 310 | 311 | #### PostGIS Installation 312 | If you've installed Postgres App as in the example above, you can easily 313 | extend it to include PostGIS. Follow these steps to begin using PostGIS: 314 | 315 | 1. Ensure that you're logged in as a user OTHER THAN `postgres`. Follow the 316 | steps above to enable your default user to be able to access the `psql` prompt. 317 | (_[installation step 7](#installation)_) 318 | 319 | 2. Type the following into the `psql` prompt to add the extension: 320 | `CREATE EXTENSION postgis;` 321 | 322 | #### PostGIS Distance between two sets of coordinates 323 | 324 | After you've extended PostgreSQL with PostGIS you can begin to use it. Type 325 | the following command into the `psql` command line: 326 | 327 | ```sql 328 | SELECT ST_Distance(gg1, gg2) As spheroid_dist 329 | FROM (SELECT 330 | ST_GeogFromText('SRID=4326;POINT(-72.1235 42.3521)') As gg1, 331 | ST_GeogFromText('SRID=4326;POINT(-72.1235 43.1111)') As gg2 332 | ) As foo ; 333 | ``` 334 | 335 | This should return `spheroid_dist` along with a value in meters. The 336 | example above returns: `84315.42034614` which is rougly 84.3km between the two 337 | points. 338 | 339 | ### Commands 340 | Once you are serving the database from your computer 341 | 342 | - To change db 343 | `\connect database_name;` 344 | 345 | - To see the tables in the database 346 | `\d;` 347 | 348 | - To select (and show in terminal) all tables 349 | `SELECT * FROM table_name` 350 | 351 | 352 | - To make a table 353 | `CREATE TABLE table_name (col_name1, col_name2)` 354 | 355 | - To add a row 356 | `INSERT INTO table_name ( col_name ) 357 | VALUES ( col_value)` 358 | col_name only require if only some of the cols are being filled out 359 | 360 | - To edit a column to a table  361 | `ALTER TABLE table_name 362 |   ALTER COLUMN column_name SET DEFAULT expression` 363 | 364 | - To add a column to a table  365 | `ALTER TABLE table_name 366 |   ADD COLUMN column_name data_type` 367 | 368 | - To find the number of instances where the word “Day” is present in the title of a table 369 | `SELECT count(title) FROM table_name WHERE title LIKE '%Day%’;` 370 | 371 | - To delete a row in a table 372 | `DELETE FROM table_name 373 | WHERE column_name = ‘hello';` 374 | 375 | 376 | Postgresql follows the SQL convention of calling relations TABLES, attributes COLUMNs and tuples ROWS 377 | 378 | **Transaction** 379 | All or nothing, if something fails the other commands are rolled back like nothing happened 380 | 381 | **Reference** 382 | When a table is being created you can reference a column in another table to make sure any value which is added to that column exists in the referenced table. 383 | 384 | ```sql 385 | CREATE TABLE cities ( 386 | name text NOT NULL, 387 | postal_code varchar(9) CHECK (postal_code <> ''), 388 | country_code char(2) REFERENCES countries, 389 | PRIMARY KEY (country_code, postal_code) 390 | ); 391 | ``` 392 | 393 | `<>` means not equal 394 | 395 | 396 | **Join reads** 397 | You can join tables together when reading them, 398 | 399 | **Inner Join** 400 | Joins together two tables by specifying a column in each to join them by i.e. 401 | 402 | ```sql 403 | SELECT cities.*, country_name 404 | FROM cities INNER JOIN countries 405 | ON cities.country_code = countries.country_code; 406 | ``` 407 | 408 | This will select all of the columns in both the countries 409 | and cities tables the data, the rows are matched up by `country_code`. 410 | 411 | **Grouping** 412 | You can put rows into groups where the group is defined by a shared value in a particular column. 413 | 414 | ```sql 415 | SELECT venue_id, count(*) 416 | FROM events 417 | GROUP BY venue_id; 418 | ``` 419 | 420 | This will group the rows together by the venue_id, 421 | count is then performed on each of the groups. 422 | 423 | ### Learning Resources 424 | 425 | + Node-hero: https://blog.risingstack.com/node-js-database-tutorial 426 | + Pluralsight postgres getting started: 427 | https://www.pluralsight.com/courses/postgresql-getting-started 428 | + Tech Republic Postgres setup: 429 | https://www.techrepublic.com/article/diy-a-postgresql-database-server-setup-anyone-can-handle/ 430 | + PostGIS installation: https://postgis.net/install 431 | + PostGIS docs: https://postgis.net/docs/manual-2.3 432 | + SQl Tutorials: https://www.scaler.com/topics/sql/ 433 | + PostGIS ST_Distance: https://postgis.net/docs/ST_Distance.html 434 | + Foreign Key Constraints: 435 | + https://en.wikipedia.org/wiki/Foreign_key 436 | + https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-foreign-key/ 437 | + https://tableplus.io/blog/2018/08/postgresql-how-to-add-a-foreign-key.html 438 | + Graphical Interface (GUI) tools: 439 | https://wiki.postgresql.org/wiki/Community_Guide_to_PostgreSQL_GUI_Tools 440 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Time MVP (Title Gets Over-ridden with Clock ;-) 5 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Hello World 123!

14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /install.md: -------------------------------------------------------------------------------- 1 | # DBeaver 2 | 3 | See: https://github.com/dwyl/learn-postgresql/issues/43#issuecomment-469000357 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learn-postgresql", 3 | "version": "1.0.0", 4 | "description": "PostgreSQL Tutorial", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "drop": "psql -U postgres -c 'DROP DATABASE IF EXISTS codeface;'", 11 | "create": "psql -U postgres -c 'CREATE DATABASE codeface;'", 12 | "schema": "psql -U postgres -d codeface -a -f schema.sql", 13 | "recreate": "npm run drop && npm run create && npm run schema", 14 | "test": "nyc tap ./test/*.test.js | tap-nyc", 15 | "quick": "tap ./test/*.test.js", 16 | "start": "node server/server.js", 17 | "faster": "./node_modules/faster/bin/faster.js", 18 | "postinstall": "npm run recreate", 19 | "open-cov": "open ./coverage/lcov-report/index.html" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/dwyl/learn-postgresql.git" 24 | }, 25 | "keywords": [ 26 | "PostgreSQL", 27 | "Postgres", 28 | "Beginners", 29 | "Tutorial" 30 | ], 31 | "author": "dwyl & friends", 32 | "license": "GPL-2.0", 33 | "bugs": { 34 | "url": "https://github.com/dwyl/learn-postgresql/issues" 35 | }, 36 | "homepage": "https://github.com/dwyl/learn-postgresql#readme", 37 | "dependencies": { 38 | "github-scraper": "^6.7.0", 39 | "pg": "^7.8.1" 40 | }, 41 | "devDependencies": { 42 | "faster": "^3.5.1", 43 | "nyc": "^13.1.0", 44 | "supertest": "^4.0.2", 45 | "tap": "^12.6.1", 46 | "tap-nyc": "^1.0.3" 47 | }, 48 | "nyc": { 49 | "check-coverage": true, 50 | "lines": 100, 51 | "statements": 100, 52 | "functions": 100, 53 | "branches": 100, 54 | "include": [ 55 | "server/*.js" 56 | ], 57 | "exclude": [ 58 | "test/*.test.js" 59 | ], 60 | "reporter": [ 61 | "lcov", 62 | "text-summary" 63 | ], 64 | "cacheDirectories": [ 65 | "node_modules" 66 | ], 67 | "all": true, 68 | "report-dir": "./coverage" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /query.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | next_page, 3 | COUNT (next_page) AS c 4 | FROM 5 | logs 6 | WHERE next_page IS NOT null 7 | AND next_page NOT IN ( 8 | SELECT path 9 | FROM logs 10 | WHERE path IS NOT NULL 11 | ) 12 | GROUP BY 13 | next_page 14 | ORDER BY 15 | c ASC 16 | LIMIT 1; 17 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "people" ( 2 | "inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 3 | "id" SERIAL PRIMARY KEY, 4 | "name" VARCHAR(50) DEFAULT NULL, 5 | "username" VARCHAR(50) NOT NULL, 6 | "bio" VARCHAR(255) DEFAULT NULL, 7 | "worksfor" VARCHAR(50) DEFAULT NULL, 8 | "uid" INT NOT NULL, -- the person's GitHub uid e.g: 4185328 9 | "location" VARCHAR(100) DEFAULT NULL, 10 | "website" VARCHAR(255) DEFAULT NULL, 11 | "stars" INT DEFAULT 0, 12 | "followers" INT DEFAULT 0, 13 | "following" INT DEFAULT 0, 14 | "contribs" INT DEFAULT 0, 15 | "recent_activity" INT DEFAULT 0 16 | ); 17 | 18 | CREATE TABLE IF NOT EXISTS "orgs" ( 19 | "inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | "id" SERIAL PRIMARY KEY, 21 | "name" VARCHAR(50) DEFAULT NULL, 22 | "url" VARCHAR(50), 23 | "description" VARCHAR(255) DEFAULT NULL, 24 | "location" VARCHAR(50) DEFAULT NULL, 25 | "website" VARCHAR(255) DEFAULT NULL, 26 | "email" VARCHAR(255) DEFAULT NULL, 27 | "pcount" INT DEFAULT 0, 28 | "uid" INT NOT NULL 29 | ); 30 | 31 | CREATE TABLE IF NOT EXISTS "repos" ( 32 | "inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | "id" SERIAL PRIMARY KEY, 34 | "url" VARCHAR(255) NOT NULL, -- know what the char limit is for a repo name? 35 | "description" TEXT DEFAULT NULL, 36 | "website" VARCHAR(255) DEFAULT NULL, 37 | "watchers" INT DEFAULT 0, 38 | "stars" INT DEFAULT 0, 39 | "forks" INT DEFAULT 0, 40 | "commits" INT DEFAULT 0, 41 | "langs" VARCHAR(255) DEFAULT NULL, 42 | "tags" TEXT DEFAULT NULL, 43 | "person_id" INT REFERENCES people (id), -- can be NULL if repo belongs to org. 44 | "org_id" INT REFERENCES orgs (id) -- this can be NULL if repo is personal. 45 | ); 46 | 47 | CREATE TABLE IF NOT EXISTS "logs" ( 48 | "id" SERIAL PRIMARY KEY, 49 | "inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 50 | "url" VARCHAR(255) NOT NULL, 51 | "next_page" VARCHAR(255) DEFAULT NULL 52 | ); 53 | 54 | CREATE TABLE IF NOT EXISTS "relationships" ( 55 | "inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 56 | "id" SERIAL PRIMARY KEY, 57 | "person_id" INT REFERENCES people (id) DEFAULT NULL, 58 | "leader_id" INT REFERENCES people (id) DEFAULT NULL, 59 | "org_id" INT REFERENCES orgs (id) DEFAULT NULL, 60 | "repo_id" INT REFERENCES repos (id) DEFAULT NULL 61 | ); 62 | -------------------------------------------------------------------------------- /server/bot.js: -------------------------------------------------------------------------------- 1 | const db = require('./db'); 2 | const utils = require('./utils'); 3 | const gs = require('github-scraper'); 4 | 5 | function fetch (path, callback) { 6 | gs(path, function(error, data) { 7 | if (error) { // don't bother trying to save data if an error occurred 8 | utils.log_error(error, data, new Error().stack); // get exact stack trace. 9 | return utils.exec_cb(callback, error, data); 10 | } 11 | console.log('data.type:', data.type); 12 | switch (data.type) { 13 | case 'org': 14 | db.insert_org(data, callback); 15 | break; 16 | case 'profile': 17 | db.insert_person(data, callback); 18 | break; 19 | case 'repo': 20 | db.insert_repo(data, callback); 21 | break; 22 | case 'followers': // multiple cases same outcome. 23 | case 'following': 24 | case 'people': 25 | case 'stars': 26 | fetch_list_of_profiles_slowly(data, callback); 27 | break; 28 | } 29 | }); 30 | } 31 | 32 | /** 33 | * fetch_list_of_profiles_slowly does what it's name suggests. 34 | * attempting to fetch GitHub profiles too quickly results in errors. 35 | * @param {object} data - should contain url and entries (a list of people). 36 | * @param {function} next - the function executed once profiles are saved. 37 | * @param {function} callback - the callback function to be executed if any. 38 | */ 39 | function fetch_list_of_profiles_slowly (data, callback) { 40 | const len = data.entries.length; 41 | 42 | data.entries.forEach((u, i) => { // poor person's "async parallel": 43 | 44 | setTimeout(function delayed_request () { // delay requests to avoid errors 45 | 46 | gs(u.username, function process (error, profile) { 47 | utils.log_error(error, profile, new Error().stack); 48 | 49 | db.insert_person(profile, function (err2, data2) { 50 | 51 | if (i == len - 1) { // only insert relationships once people records 52 | return db.insert_relationships(data, callback); // once per batch. 53 | } // e.g: db.insert_stars(data, callback) in the case of 'stars' page 54 | }); 55 | }); 56 | }, i * 1000); // timer gets longer as i increases to avoid flooding! 57 | }); 58 | } 59 | 60 | module.exports = { 61 | fetch: fetch 62 | } 63 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | process.env.DATABASE_URL = process.env.DATABASE_URL 3 | || "postgres://postgres:@localhost/codeface"; 4 | const pg = require('pg'); 5 | const PG_CLIENT = new pg.Client(process.env.DATABASE_URL); 6 | const utils = require('./utils'); 7 | 8 | console.log('db.js:L7: PG_CLIENT._connecting:', PG_CLIENT._connecting, // debug 9 | '| PG_CLIENT._connected:', PG_CLIENT._connected); 10 | 11 | // auto-start pg connection when module is required so startup is faster! 12 | connect(function (err, data) { 13 | console.log('db.js:L12: PG_CLIENT._connected:', PG_CLIENT._connected); 14 | console.log('- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -'); 15 | }) 16 | 17 | /** 18 | * connnect ensures that a postgres connection is available before continuing 19 | * @param {function} callback - function called once connection is confirmed. 20 | */ 21 | function connect (callback) { 22 | // console.log('L45: PG_CLIENT._connecting:', PG_CLIENT._connecting, 23 | // '| PG_CLIENT._connected:', PG_CLIENT._connected); 24 | if (PG_CLIENT && !PG_CLIENT._connected && !PG_CLIENT._connecting) { 25 | PG_CLIENT.connect(function (error, data) { 26 | utils.log_error(error, data, new Error().stack); 27 | return utils.exec_cb(callback, error, PG_CLIENT); 28 | }); 29 | } else { 30 | return utils.exec_cb(callback, null, PG_CLIENT); 31 | } 32 | } 33 | 34 | /** 35 | * end used in testing to end/close the Postgres connection: 36 | * @param {function} callback - callback function to be executed on success. 37 | */ 38 | function end (callback) { 39 | /* istanbul ignore else */ 40 | if(PG_CLIENT && PG_CLIENT._connected && !PG_CLIENT._connecting) { 41 | PG_CLIENT.end(() => { 42 | return utils.exec_cb (callback, null, PG_CLIENT); 43 | }); 44 | } 45 | } 46 | 47 | /** 48 | * insert_person saves a person's data to the people table. 49 | * @param {object} data - a valid JSON object containing data to be inserted. 50 | * @param {function} callback - callback function to be executed on success. 51 | */ 52 | function insert_person (data, callback) { 53 | connect( function insert_person_after_connected () { 54 | const { name, username, bio, worksfor, location, website, uid, 55 | stars, followers, following, contribs } = data; 56 | const recent_activity = utils.recent_activity(data); 57 | const query = `INSERT INTO people (name, username, bio, worksfor, location, 58 | website, uid, stars, followers, following, contribs, recent_activity) 59 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`; 60 | const values = [name, username, bio, worksfor, location, website, uid, 61 | stars, followers, following, contribs, recent_activity]; 62 | 63 | PG_CLIENT.query(query, values, function(error, result) { 64 | utils.log_error(error, data, new Error().stack); 65 | return insert_next_page (data, callback); 66 | }); 67 | }); 68 | } 69 | 70 | /** 71 | * select_person gets the person from people table. 72 | * @param {string} username - username of the person e.g: 'iteles' 73 | * @param {function} callback - callback function to be executed on success. 74 | */ 75 | function select_person (username, callback) { 76 | connect( function select_person_after_connected () { 77 | const query = `SELECT * FROM people WHERE username = $1 78 | ORDER BY id ASC LIMIT 1`; 79 | PG_CLIENT.query(query, [username], function(error, result) { 80 | utils.log_error(error, result, new Error().stack); 81 | return utils.exec_cb(callback, error, result); 82 | }); 83 | }); 84 | } 85 | 86 | /** 87 | * insert_org saves an org's data to the orgs table. 88 | * 89 | */ 90 | function insert_org (data, callback) { 91 | connect( function insert_org_after_connected () { 92 | const { url,name,description,location,website,email,pcount,uid } = data; 93 | const query = `INSERT INTO orgs 94 | (url, name, description, location, website, email, pcount, uid) 95 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`; 96 | const values = [url,name,description,location,website,email,pcount,uid]; 97 | 98 | PG_CLIENT.query(query, values, function(error, result) { 99 | utils.log_error(error, data, new Error().stack); 100 | return insert_next_page (data, callback); 101 | }); 102 | }); 103 | } 104 | 105 | /** 106 | * select_org retrieves the org for a given url. 107 | * @param {string} url - url of the repo (e.g: /dwyl) 108 | * @param {function} callback - callback function to be executed on success. 109 | */ 110 | function select_org (url, callback) { 111 | connect( function select_repo_after_connected () { 112 | const query = `SELECT * FROM orgs WHERE url = $1 ORDER BY id ASC LIMIT 1`; 113 | console.log(query, url); 114 | PG_CLIENT.query(query, [url], function(error, result) { 115 | utils.log_error(error, result, new Error().stack); 116 | return utils.exec_cb(callback, error, result); 117 | }); 118 | }); 119 | } 120 | 121 | /** 122 | * insert_repo saves an repo's stats to the repos table. 123 | * @param {object} data - a valid JSON object containing data to be inserted. 124 | * @param {function} callback - callback function to be executed on success. 125 | */ 126 | function insert_repo (data, callback) { 127 | connect( function insert_repo_after_connected () { 128 | const { url, description, website, tags, langs, 129 | watchers, stars, forks, commits} = data; 130 | const query = `INSERT INTO repos 131 | (url, description, website, tags, langs, watchers, stars, forks, commits) 132 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`; 133 | const values = [url, description, website, tags, langs.join(','), 134 | watchers, stars, forks, commits]; 135 | 136 | PG_CLIENT.query(query, values, function(error, result) { 137 | utils.log_error(error, data, new Error().stack); 138 | return insert_next_page (data, callback); 139 | }); 140 | }); 141 | } 142 | 143 | /** 144 | * select_repo retrieves the repo for a given url. 145 | * @param {string} url - url of the repo (e.g: /dwyl/start-here) 146 | * @param {function} callback - callback function to be executed on success. 147 | */ 148 | function select_repo (url, callback) { 149 | connect( function select_repo_after_connected () { 150 | const query = `SELECT * FROM repos WHERE url = $1 ORDER BY id ASC LIMIT 1`; 151 | url = url.replace('/stargazers', ''); 152 | PG_CLIENT.query(query, [url], function(error, result) { 153 | utils.log_error(error, result, new Error().stack); 154 | return utils.exec_cb(callback, error, result); 155 | }); 156 | }); 157 | } 158 | 159 | /** 160 | * insert_relationship saves the list of people who related to another record. 161 | * @param {object} data - a valid JSON object containing data to be inserted. 162 | * @param {function} callback - callback function to be executed on success. 163 | */ 164 | function insert_relationships (data, callback) { 165 | let fields, rel_id, url, username; 166 | const len = data.entries.length - 1; 167 | 168 | function insert_rows () { // inner function has access to outer variables 169 | data.entries.forEach((p, i) => { // poor person's "async parallel": 170 | const username = p.username; 171 | // console.log('username:', username); 172 | select_person(username, function(error1, result1) { 173 | // console.log('L251 > result1: ', result1.rows[0]); 174 | const person_id = result1.rows[0].id; 175 | const query = `INSERT INTO relationships (${fields}) VALUES ($1, $2)` 176 | const values = [person_id, rel_id]; 177 | // console.log('query:', query, 'values:', values); 178 | PG_CLIENT.query(query, values, function(error2, result2) { 179 | utils.log_error(error2, result2, new Error().stack); 180 | 181 | if(i === len) { 182 | return insert_next_page(data, callback); 183 | } 184 | }); 185 | }); 186 | }); // END data.entries.forEach 187 | } 188 | // there are three types of relationships, we switch based on data.type 189 | switch (data.type) { 190 | case 'stars': 191 | fields = 'person_id, repo_id'; 192 | select_repo(data.url, function (error, result) { 193 | rel_id = result.rows[0].id; 194 | insert_rows(); 195 | }); // END select_repo 196 | break; 197 | case 'people': // this is a list of members of an organisation 198 | fields = 'person_id, org_id'; 199 | url = '/' + data.url.split('/')[2];// /orgs/dwyl/people > /dwyl 200 | select_org(url, function (error, result) { 201 | rel_id = result.rows[0].id; 202 | insert_rows(); 203 | }); // END select_org 204 | break; 205 | case 'followers': // this is a list of followers/following 206 | fields = 'person_id, leader_id'; 207 | username = data.url.split('/')[1]; // /dwylbot/followers > dwylbot 208 | // console.log('username', username); 209 | // list of followers: 210 | select_person(username, function (error, result) { 211 | rel_id = result.rows[0].id; 212 | insert_rows(); 213 | }); // END select_org 214 | break; 215 | case 'following': // pay attention to the subtle difference in fields order 216 | fields = 'leader_id, person_id'; 217 | username = data.url.split('/')[1]; // /dwylbot/following > dwylbot 218 | // console.log('username', username); 219 | // list of followers: 220 | select_person(username, function (error, result) { 221 | rel_id = result.rows[0].id; 222 | insert_rows(); 223 | }); // END select_org 224 | break; 225 | } 226 | } 227 | 228 | /** 229 | * insert_log_item does exactly what it's name suggests inserts a log enty 230 | * @param {String} url - the current url (page) being viewed. 231 | * @param {String} next_page - the next page to be fetched. 232 | * @param {function} callback - callback function to be executed on success. 233 | */ 234 | function insert_log_item (url, next_page, callback) { 235 | connect( function () { 236 | const query = `INSERT INTO logs (url, next_page) VALUES ($1, $2)`; 237 | const values = [url, next_page] 238 | PG_CLIENT.query(query, values, function(error, data) { 239 | utils.log_error(error, data, new Error().stack); 240 | return utils.exec_cb(callback, error, data); 241 | }); 242 | }); 243 | } 244 | 245 | function profile_next_page(urls, username) { 246 | urls.push(username + '/followers'); 247 | urls.push(username + '/following'); 248 | urls.push(username + '?tab=repositories'); 249 | return urls; 250 | } 251 | 252 | /** 253 | * insert_next_page inserts the list of next pages to be crawled. 254 | * @param {Object} data - a valid JSON object containing data to be inserted. 255 | * @param {function} callback - callback function to be executed on success. 256 | */ 257 | function insert_next_page (data, callback) { 258 | let urls = [] 259 | switch (data.type) { 260 | case 'org': 261 | // console.log('data.name', data.name); 262 | urls = data.entries.map((e) => e.url); 263 | urls.push('orgs/' + data.name + '/people'); // list of PUBLIC org members. 264 | urls.push(data.next_page); // if it exists. 265 | break; 266 | case 'profile': 267 | urls = data.pinned.map((e) => e.url); 268 | const orgs = Object.keys(data.orgs); 269 | orgs.forEach(org => urls.push(org)); 270 | urls = profile_next_page(urls, data.username); 271 | break; 272 | case 'repo': 273 | urls.push(data.url + '/stargazers'); 274 | break; 275 | case 'followers': 276 | case 'following': 277 | case 'people': 278 | case 'stars': 279 | urls.push(data.next_page); 280 | data.entries.forEach((e) => { urls = profile_next_page(urls, e.username)}) 281 | break; 282 | } 283 | let len = urls.length; 284 | urls.filter((e) => e !== null) // filter out blanks (if next_page is null) 285 | .forEach((next_page, i) => { // poor person's "async parallel": 286 | insert_log_item(data.url, next_page, (error, data2) => { 287 | if(--len == 0) { 288 | return utils.exec_cb(callback, null, data); 289 | } 290 | }) 291 | }); 292 | } 293 | 294 | /** 295 | * select_next_page get the next url (page) to crawl 296 | */ 297 | function select_next_page (callback) { 298 | connect( function () { 299 | const query = `SELECT next_page, COUNT (next_page) AS c 300 | FROM logs 301 | WHERE next_page IS NOT null 302 | AND next_page NOT IN ( 303 | SELECT url 304 | FROM logs 305 | WHERE url IS NOT NULL 306 | ) 307 | GROUP BY next_page 308 | ORDER BY c ASC 309 | LIMIT 1;`; 310 | // console.log('L82: query:', query); 311 | PG_CLIENT.query(query, function(error, data) { 312 | utils.log_error(error, data, new Error().stack); 313 | return utils.exec_cb(callback, error, data); 314 | }); 315 | }); 316 | } 317 | 318 | module.exports = { 319 | connect: connect, 320 | end: end, 321 | insert_log_item: insert_log_item, 322 | select_next_page: select_next_page, 323 | insert_person: insert_person, 324 | select_person: select_person, 325 | insert_org: insert_org, 326 | select_org: select_org, 327 | insert_repo: insert_repo, 328 | select_repo: select_repo, 329 | insert_relationships: insert_relationships, 330 | PG_CLIENT: PG_CLIENT 331 | } 332 | -------------------------------------------------------------------------------- /server/lanip.js: -------------------------------------------------------------------------------- 1 | // see: https://stackoverflow.com/questions/10750303 2 | var os = require('os'); 3 | var interfaces = os.networkInterfaces(); 4 | var ip = []; 5 | for (var k in interfaces) { 6 | for (var k2 in interfaces[k]) { 7 | var address = interfaces[k][k2]; 8 | if (address.family === 'IPv4' && !address.internal) { 9 | ip.push(address.address); 10 | } 11 | } 12 | } 13 | module.exports = ip[0]; 14 | -------------------------------------------------------------------------------- /server/request_handlers.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | // var db = require('./db.js'); 4 | var index = path.resolve(__dirname, '../client/index.html'); 5 | var app = path.resolve(__dirname, '../client/app.js'); 6 | 7 | function serve_index(req, res) { 8 | return fs.readFile(index, function (err, data) { 9 | res.writeHead(200, {'Content-Type': 'text/html'}); 10 | res.end(data); 11 | }); 12 | } 13 | 14 | // function serve_app(req, res) { 15 | // return fs.readFile(app, function (err, data) { 16 | // res.writeHead(200, {'Content-Type': 'text/javascript'}); 17 | // res.end(data); 18 | // }); 19 | // } 20 | 21 | module.exports = { 22 | serve_index: serve_index, 23 | // serve_app: serve_app, 24 | // serve_static: serve_static, 25 | // handle_post: handle_post, 26 | // handle_email_verification_request: handle_email_verification_request 27 | } 28 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | process.env.PORT = process.env.PORT || 4000; 2 | const http = require('http'); 3 | const handlers = require('./request_handlers.js'); 4 | 5 | const server = http.createServer(function run (req, res) { // can you make simplify it? ;-) 6 | console.log(req.method, ':', req.url); // absolute minimum request logging 7 | var url = req.url.split('?')[0]; // strip query params for routing 8 | switch (url) { 9 | // case '/elmo.js': // not "DRY" ... #helpwanted! 10 | // handlers.serve_static(req, res); 11 | // break; 12 | // case '/app.js': // serve the client application 13 | // handlers.serve_app(req, res); 14 | // break; 15 | // case '/save': // save state to server 16 | // handlers.handle_post(req, res); 17 | // break; 18 | default: // serve the application 19 | handlers.serve_index(req, res); 20 | break; 21 | } 22 | }).listen(process.env.PORT); // start the server with the command: npm run dev 23 | 24 | // url used in tests: 25 | server.url = "http://" + require('./lanip.js') + ":" + process.env.PORT; 26 | // show local LAN IP address in console so we can connect to the app on mobile: 27 | console.info("GOTO:", server.url); 28 | 29 | module.exports = server; 30 | -------------------------------------------------------------------------------- /server/utils.js: -------------------------------------------------------------------------------- 1 | const BG = '\x1b[44m\x1b[33m\x1b[1m'; 2 | const RESET = '\x1b[0m'; // see: https://stackoverflow.com/a/41407246/1148249 3 | 4 | /** 5 | * log_error is a basic error logger function which logs errors when present. 6 | * the beauty of centralising error logging in your apps is that it makes it 7 | * easy to change the logging to use a logging *service* or tool later 8 | * without having to change all instances of your logger. 9 | * @param {Object|String} error - the error reported 10 | * @param {Object} data - any data being passed back to the calling function. 11 | * @param {String} stack - the call stack where log_error was called from. 12 | * @example 13 | * utils.log_error(error, data, new Error().stack); // 14 | */ 15 | function log_error (error, data, stack) { 16 | stack = stack || 'remember to include `stack` (third param) in log_error!' 17 | if (error) { 18 | console.error( 19 | BG, 20 | 'ERROR:', error, stack.toString(), 21 | RESET 22 | ); // .split('\n')[1] 23 | } 24 | return; 25 | } 26 | 27 | /** 28 | * exec_cb runs a callback if it's a function avoids type error if not a func. 29 | * @param {function} callback - the callback function to be executed if any. 30 | * @param {Object|String} error - the error reported 31 | * @param {Object} data - any data being passed back to the calling function. 32 | */ 33 | function exec_cb (callback, error, data) { 34 | log_error(error, data, new Error().stack); 35 | if (callback && typeof callback === 'function') { 36 | return callback(error, data); 37 | } // if callback is undefine or not a function do nothing! 38 | return; 39 | } 40 | 41 | /** 42 | * recent_activity returns the count of recent activity for a profile 43 | */ 44 | function recent_activity(json) { 45 | const DAYS = 14; 46 | const keys = Object.keys(json["contrib_matrix"]); 47 | const len = keys.length - 1; 48 | const latest_contribs = keys.slice(- DAYS); 49 | return latest_contribs.reduce((sum, k) => { 50 | return sum + json["contrib_matrix"][k]['count'] 51 | }, 0); 52 | } 53 | 54 | module.exports = { 55 | log_error: log_error, 56 | exec_cb: exec_cb, 57 | recent_activity: recent_activity 58 | } 59 | -------------------------------------------------------------------------------- /test/bot.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | const bot = require('../server/bot'); 3 | const db = require('../server/db'); 4 | const seed = Math.floor(Math.random() * Math.floor(100000)); 5 | 6 | tap.test('crawl non-existent page to test 404', function (t) { 7 | bot.fetch('/totesamaze' + seed, function(err, data) { 8 | t.equal(err, 404, 'err: ' + err + ' (as expected ;-)'); 9 | t.end() 10 | }); 11 | }); 12 | 13 | tap.test('crawl @dwyl org', function (t) { 14 | // we must TRUNCATE the orgs table when running tests: 15 | db.PG_CLIENT.query('TRUNCATE TABLE orgs CASCADE', function (err0, result0) { 16 | t.equal(err0, null, 'no error running "TRUNCATE TABLE orgs"'); 17 | t.equal(result0.command, 'TRUNCATE', 'orgs table successfully truncated'); 18 | 19 | bot.fetch('dwyl', function(err, data) { 20 | require('./fixtures/make-fixture')('org.json', data); // keep up-to-date 21 | t.end(); 22 | }); 23 | }); 24 | }); 25 | 26 | tap.test('crawl @iteles person profile', function (t) { 27 | db.PG_CLIENT.query('TRUNCATE TABLE people CASCADE', function (err0, result0) { 28 | t.equal(err0, null, 'no error running "TRUNCATE TABLE people"'); 29 | t.equal(result0.command, 'TRUNCATE', 'people table successfully truncated'); 30 | 31 | bot.fetch('iteles', function(err, data) { 32 | // delete(data.contrib_matrix); // TMI! 33 | require('./fixtures/make-fixture')('person.json', data); 34 | t.end() 35 | }); 36 | 37 | }); // end TRUNCATE 38 | }); 39 | 40 | tap.test('crawl dwyl/todo-list-javascript-tutorial', function (t) { 41 | db.PG_CLIENT.query('TRUNCATE TABLE repos CASCADE', function (err0, result0) { 42 | t.equal(err0, null, 'no error running "TRUNCATE TABLE repos"'); 43 | t.equal(result0.command, 'TRUNCATE', 'repos table successfully truncated'); 44 | 45 | bot.fetch('dwyl/todo-list-javascript-tutorial', function(err, data) { 46 | require('./fixtures/make-fixture')('repo.json', data); 47 | t.end() 48 | }); 49 | }); // end TRUNCATE 50 | }); 51 | 52 | tap.test('crawl dwyl/health', function (t) { 53 | // db.PG_CLIENT.query('TRUNCATE TABLE repos CASCADE', function (err0, result0) { 54 | // t.equal(err0, null, 'no error running "TRUNCATE TABLE repos"'); 55 | // t.equal(result0.command, 'TRUNCATE', 'repos table successfully truncated'); 56 | 57 | bot.fetch('dwyl/health', function(err, data) { 58 | require('./fixtures/make-fixture')('repo.json', data); 59 | 60 | const select = 'SELECT * FROM repos ORDER by id DESC LIMIT 1'; 61 | db.PG_CLIENT.query(select, function(err, result) { 62 | t.equal(result.rows[0].url, data.url, 'repo.url ' + data.url); 63 | t.end(); 64 | }); 65 | }); 66 | // }); // end TRUNCATE 67 | }); 68 | 69 | tap.test('crawl /dwyl/health/stargazers', function (t) { 70 | bot.fetch('/dwyl/health/stargazers', function(err, data) { 71 | require('./fixtures/make-fixture')('stargazers.json', data); 72 | t.end() 73 | }); 74 | }); 75 | 76 | tap.test('crawl org members /orgs/SafeLives/people (3?)', function (t) { 77 | bot.fetch('/SafeLives', function(err1, data1) { // first store the org 78 | bot.fetch('/orgs/SafeLives/people', function(err, data) { 79 | require('./fixtures/make-fixture')('members.json', data); 80 | // console.log(data); 81 | t.equal(data.entries.length, 3, '/orgs/SafeLives/people has 3 people.'); 82 | t.end() 83 | }); 84 | }); 85 | }); 86 | 87 | tap.test('crawl /dwylbot/followers (expect 1)', function (t) { 88 | bot.fetch('/dwylbot', function(err1, data1) { // first fetch the profile 89 | bot.fetch('/dwylbot/followers', function(err, data) { 90 | require('./fixtures/make-fixture')('followers.json', data); 91 | // console.log(data); 92 | t.equal(data.entries.length, 4, '/dwylbot/following is following Simon.'); 93 | t.end() 94 | }); 95 | }); 96 | }); 97 | 98 | 99 | tap.test('crawl /dwylbot/following (expect 1)', function (t) { 100 | bot.fetch('/dwylbot', function(err1, data1) { // first fetch the profile 101 | bot.fetch('/dwylbot/following', function(err, data) { 102 | require('./fixtures/make-fixture')('following.json', data); 103 | // console.log(data); 104 | t.equal(data.entries.length, 1, '/dwylbot/following is following Simon.'); 105 | t.end() 106 | }); 107 | }); 108 | }); 109 | 110 | tap.test('db.end() close database connection so tests can finish', function(t) { 111 | db.end(function(err, data) { 112 | t.equal(db.PG_CLIENT._ending, true, 113 | 'db.PG_CLIENT._ending: ' + db.PG_CLIENT._ending); 114 | t.end(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/db.test.js: -------------------------------------------------------------------------------- 1 | process.env.DATABASE_URL = process.env.DATABASE_URL 2 | || "postgres://postgres:@localhost/codeface"; 3 | 4 | const tap = require('tap'); // see: github.com/dwyl/learn-tape 5 | const db = require('../server/db'); 6 | 7 | const seed = Math.floor(Math.random() * Math.floor(100000)); 8 | const url = '/dwyl'; 9 | 10 | tap.test('db.select_next_page selects next_page to be viewed', function(t) { 11 | db.PG_CLIENT.query('TRUNCATE TABLE logs', function (err0, result0) { 12 | t.equal(err0, null, 'no error running "TRUNCATE TABLE logs"'); 13 | t.equal(result0.command, 'TRUNCATE', 'logs table successfully truncated'); 14 | 15 | db.insert_log_item(url, url + seed, function (err, result) { 16 | const select = 'SELECT * FROM logs ORDER by id DESC LIMIT 1'; 17 | db.PG_CLIENT.query(select, function(err, result) { 18 | // console.log(result); 19 | t.equal(result.rows[0].url, url, 'logs.url is ' + url); 20 | t.end(); 21 | }); 22 | }); 23 | }); 24 | }); 25 | 26 | tap.test('db.select_next_page selects next_page to be viewed', function(t) { 27 | db.select_next_page(function (err, result) { 28 | t.equal(result.rows[0].next_page, url + seed, 29 | 'next_page is: ' + result.rows[0].next_page); 30 | t.end(); 31 | }); 32 | }); 33 | 34 | 35 | tap.test('insert_person insert test/fixtures/person.json data', function(t) { 36 | const person = require('./fixtures/person.json'); 37 | db.insert_person(person, function (err, result) { 38 | db.select_person(person.username, function(err, result) { 39 | t.equal(result.rows[0].name, person.name, 'person.name ' + person.name); 40 | t.end(); 41 | }); 42 | }); 43 | }); 44 | 45 | tap.test('insert_org', function(t) { 46 | const org = require('./fixtures/org.json'); 47 | // given that we have a uniqueness constraint on the name and uid fields 48 | // we must TRUNCATE the orgs table when running tests: 49 | db.PG_CLIENT.query('TRUNCATE TABLE orgs CASCADE', function (err2, result2) { 50 | 51 | db.insert_org(org, function (err, result) { 52 | const select = 'SELECT * FROM orgs ORDER by id DESC LIMIT 1'; 53 | db.PG_CLIENT.query(select, function(err, result) { 54 | t.equal(result.rows[0].uid, org.uid, 'org.uid ' + org.uid); 55 | t.equal(result.rows[0].name, org.name, 'org.name ' + org.name); 56 | t.end(); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | tap.test('select_repo', function(t) { 63 | const repo = require('./fixtures/repo.json'); 64 | db.insert_repo(repo, function (err, result) { 65 | db.select_repo(repo.url, function (err1, result1) { 66 | t.equal(result1.rows[0].url, repo.url, 'repo.url ' + repo.url); 67 | t.end(); 68 | }); 69 | }); 70 | }); 71 | 72 | tap.test('insert_relationships', function(t) { 73 | const stars = require('./fixtures/stargazers.json'); 74 | db.insert_relationships(stars, function (err0, result0) { // insert all "stars" 75 | 76 | const repo_url = stars.url.replace('/stargazers', ''); // e.g: /dwyl/health 77 | 78 | db.select_repo(repo_url, function (err1, data1) { 79 | 80 | const repo_id = data1.rows[0].id; 81 | console.log('repo_id:', repo_id); 82 | const username = stars.entries[0].username; // e.g: SimonLab 83 | console.log('username:', username); 84 | 85 | db.select_person(username, function (err2, data2) { 86 | const person_id = data2.rows[0].id; 87 | const select = `SELECT * FROM relationships 88 | WHERE person_id = $1 AND repo_id = $2 89 | ORDER by inserted_at DESC LIMIT 1`; 90 | 91 | db.PG_CLIENT.query(select, [person_id, repo_id], function(err, result) { 92 | t.equal(result.rowCount, 1, '"stars" relationship inserted'); 93 | t.end(); 94 | }); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | tap.test('db.end() close database connection so tests can finish', function(t) { 101 | db.end(function(err, data) { 102 | t.equal(db.PG_CLIENT._ending, true, 103 | 'db.PG_CLIENT._ending: ' + db.PG_CLIENT._ending); 104 | t.end(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/fixtures/followers.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "avatar": "https://avatars1.githubusercontent.com/u/5723781?s=88&v=4", 5 | "uid": 5723781, 6 | "username": "melomg" 7 | }, 8 | { 9 | "avatar": "https://avatars0.githubusercontent.com/u/15983736?s=88&v=4", 10 | "uid": 15983736, 11 | "username": "samhstn" 12 | }, 13 | { 14 | "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=88&v=4", 15 | "uid": 6057298, 16 | "username": "SimonLab" 17 | }, 18 | { 19 | "avatar": "https://avatars1.githubusercontent.com/u/772937?s=88&v=4", 20 | "uid": 772937, 21 | "username": "ryanpcmcquen" 22 | } 23 | ], 24 | "url": "/dwylbot/followers", 25 | "type": "followers" 26 | } -------------------------------------------------------------------------------- /test/fixtures/following.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=88&v=4", 5 | "uid": 6057298, 6 | "username": "SimonLab" 7 | } 8 | ], 9 | "url": "/dwylbot/following", 10 | "type": "following" 11 | } -------------------------------------------------------------------------------- /test/fixtures/make-fixture.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = function (filename, data) { 5 | filename = path.resolve('./test/fixtures/' + filename); 6 | fs.writeFileSync(filename, JSON.stringify(data, null, 2), 'utf8'); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/members.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "avatar": "https://avatars0.githubusercontent.com/u/7805691?s=96&v=4", 5 | "uid": 7805691, 6 | "username": "harrygfox" 7 | }, 8 | { 9 | "avatar": "https://avatars1.githubusercontent.com/u/4185328?s=96&v=4", 10 | "uid": 4185328, 11 | "username": "iteles" 12 | }, 13 | { 14 | "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=96&v=4", 15 | "uid": 6057298, 16 | "username": "SimonLab" 17 | } 18 | ], 19 | "url": "/orgs/SafeLives/people", 20 | "type": "people" 21 | } -------------------------------------------------------------------------------- /test/fixtures/org.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "/dwyl", 3 | "type": "org", 4 | "name": "dwyl", 5 | "description": "Start here: https://github.com/dwyl/start-here", 6 | "location": "London, UK", 7 | "website": "https://dwyl.com", 8 | "email": "hello+github@dwyl.com", 9 | "pcount": 171, 10 | "avatar": "https://avatars1.githubusercontent.com/u/11708465?s=200&v=4", 11 | "uid": 11708465, 12 | "entries": [ 13 | { 14 | "name": "learn-postgresql", 15 | "lang": "", 16 | "url": "/dwyl/learn-postgresql", 17 | "description": "🐘 Learn how to use PostgreSQL and Structured Query Language (SQL) to store and query your relational data. 🔍", 18 | "updated": "2019-04-15T13:09:30Z" 19 | }, 20 | { 21 | "name": "learn-json-web-tokens", 22 | "lang": "JavaScript", 23 | "url": "/dwyl/learn-json-web-tokens", 24 | "description": "🔐 Learn how to use JSON Web Token (JWT) to secure your next Web App! (Tutorial/Example with Tests!!)", 25 | "updated": "2019-04-15T10:42:01Z" 26 | }, 27 | { 28 | "name": "learn-hapi", 29 | "lang": "HTML", 30 | "url": "/dwyl/learn-hapi", 31 | "description": "☀️ Learn to use Hapi.js (Node.js) web framework to build scalable apps in less time", 32 | "updated": "2019-04-13T14:23:26Z" 33 | }, 34 | { 35 | "name": "aws-sdk-mock", 36 | "lang": "JavaScript", 37 | "url": "/dwyl/aws-sdk-mock", 38 | "description": "🌈 AWSomocks for Javascript/Node.js aws-sdk tested, documented & maintained. Contributions welcome!", 39 | "updated": "2019-04-12T08:36:15Z" 40 | }, 41 | { 42 | "name": "learn-elixir", 43 | "lang": "Elixir", 44 | "url": "/dwyl/learn-elixir", 45 | "description": "💧 Learn the Elixir programming language to build functional, fast, scalable and maintainable web applications!", 46 | "updated": "2019-04-08T07:58:17Z" 47 | }, 48 | { 49 | "name": "hapi-error", 50 | "lang": "JavaScript", 51 | "url": "/dwyl/hapi-error", 52 | "description": "☔️ Intercept errors in your Hapi Web App/API and send a *useful* message to the client OR redirect to the desired endpoint.", 53 | "updated": "2019-04-07T12:52:07Z" 54 | }, 55 | { 56 | "name": "home", 57 | "lang": "", 58 | "url": "/dwyl/home", 59 | "description": "🏡 👩‍💻 💡 home is where you can [learn to] build the future surrounded by like-minded creative, friendly and [intrinsically] motivated people focussed on health, fitness and making things people and the world need!", 60 | "updated": "2019-03-30T22:05:51Z" 61 | }, 62 | { 63 | "name": "elixir-dojo", 64 | "lang": "Elixir", 65 | "url": "/dwyl/elixir-dojo", 66 | "description": "Identify card hands", 67 | "updated": "2019-03-30T18:58:53Z" 68 | }, 69 | { 70 | "name": "learn-tape", 71 | "lang": "JavaScript", 72 | "url": "/dwyl/learn-tape", 73 | "description": "✅ Learn how to use Tape for JavaScript/Node.js Test Driven Development (TDD) - Ten-Minute Testing Tutorial", 74 | "updated": "2019-03-30T13:31:23Z" 75 | }, 76 | { 77 | "name": "learn-travis", 78 | "lang": "JavaScript", 79 | "url": "/dwyl/learn-travis", 80 | "description": "😎 A quick Travis CI (Continuous Integration) Tutorial for Node.js developers", 81 | "updated": "2019-03-30T13:15:15Z" 82 | }, 83 | { 84 | "name": "ordem", 85 | "lang": "JavaScript", 86 | "url": "/dwyl/ordem", 87 | "description": "🏁 ultra-simple ordered task runner for Node.js and Browser. Run your asynchronous functions predictably in series.", 88 | "updated": "2019-03-29T20:26:27Z" 89 | }, 90 | { 91 | "name": "learn-elm-architecture-in-javascript", 92 | "lang": "JavaScript", 93 | "url": "/dwyl/learn-elm-architecture-in-javascript", 94 | "description": "🦄 Learn how to build web apps using the Elm Architecture in \"vanilla\" JavaScript (step-by-step TDD tutorial)!", 95 | "updated": "2019-03-29T09:20:26Z" 96 | }, 97 | { 98 | "name": "todo-list-javascript-tutorial", 99 | "lang": "JavaScript", 100 | "url": "/dwyl/todo-list-javascript-tutorial", 101 | "description": "✅ A step-by-step complete beginner example/tutorial for building a Todo List App (TodoMVC) from scratch in JavaScript following Test Driven Development (TDD) best practice. 🌱", 102 | "updated": "2019-03-28T21:13:46Z" 103 | }, 104 | { 105 | "name": "process-handbook", 106 | "lang": "", 107 | "url": "/dwyl/process-handbook", 108 | "description": "📗 Contains our processes, questions and journey to creating ateam", 109 | "updated": "2019-03-27T21:49:19Z" 110 | }, 111 | { 112 | "name": "auth", 113 | "lang": "Elixir", 114 | "url": "/dwyl/auth", 115 | "description": "🚪 🔐 A Complete Authentication Solution for Elixir/Phoenix Web Apps/APIs (Documented, Tested & Maintained)", 116 | "updated": "2019-03-26T11:50:37Z" 117 | }, 118 | { 119 | "name": "learn-phoenix-framework", 120 | "lang": "Elixir", 121 | "url": "/dwyl/learn-phoenix-framework", 122 | "description": "🔥 Phoenix is the web framework without compromise on speed, reliability or maintainability! Don't settle for less. 🚀", 123 | "updated": "2019-03-26T11:49:30Z" 124 | }, 125 | { 126 | "name": "phoenix-ecto-append-only-log-example", 127 | "lang": "Elixir", 128 | "url": "/dwyl/phoenix-ecto-append-only-log-example", 129 | "description": "📝 A step-by-step example/tutorial showing how to build a Phoenix (Elixir) App where all data is immutable (append only). Precursor to Blockchain, IPFS or Solid!", 130 | "updated": "2019-03-25T05:50:07Z" 131 | }, 132 | { 133 | "name": "hapi-auth-jwt2", 134 | "lang": "JavaScript", 135 | "url": "/dwyl/hapi-auth-jwt2", 136 | "description": "🔒 Secure Hapi.js authentication plugin using JSON Web Tokens (JWT) in Headers, Query or Cookies", 137 | "updated": "2019-03-24T19:51:17Z" 138 | }, 139 | { 140 | "name": "learn-docker", 141 | "lang": "Dockerfile", 142 | "url": "/dwyl/learn-docker", 143 | "description": "🚢 Learn how to use docker.io containers to consistently deploy your apps on any infrastructure.", 144 | "updated": "2019-03-21T12:12:23Z" 145 | }, 146 | { 147 | "name": "sendemail", 148 | "lang": "JavaScript", 149 | "url": "/dwyl/sendemail", 150 | "description": "✉️ Simplifies reliably sending emails from your node.js apps using AWS Simple Email Service (SES)", 151 | "updated": "2019-03-20T15:35:36Z" 152 | }, 153 | { 154 | "name": "learn-heroku", 155 | "lang": "HTML", 156 | "url": "/dwyl/learn-heroku", 157 | "description": "🏁 Learn how to deploy your web application to Heroku from scratch step-by-step in 7 minutes!", 158 | "updated": "2019-03-14T11:37:37Z" 159 | }, 160 | { 161 | "name": "learn-wireframing", 162 | "lang": "", 163 | "url": "/dwyl/learn-wireframing", 164 | "description": "💡 📰 Learn how to share your UX ideas with your team and the world so you can test hypotheses fast!", 165 | "updated": "2019-03-13T20:44:36Z" 166 | }, 167 | { 168 | "name": "english-words", 169 | "lang": "Python", 170 | "url": "/dwyl/english-words", 171 | "description": "📝 A text file containing 479k English words for all your dictionary/word-based projects e.g: auto-completion / autosuggestion", 172 | "updated": "2019-03-13T20:35:44Z" 173 | }, 174 | { 175 | "name": "alog", 176 | "lang": "Elixir", 177 | "url": "/dwyl/alog", 178 | "description": "🌲 alog (Append-only Log) is an easy way to start using the Lambda/Kappa architecture in your Elixir/Phoenix Apps while still using PostgreSQL (with Ecto).", 179 | "updated": "2019-03-13T11:06:36Z" 180 | }, 181 | { 182 | "name": "phoenix-uk-postcode-finder-example", 183 | "lang": "Elixir", 184 | "url": "/dwyl/phoenix-uk-postcode-finder-example", 185 | "description": "📍An example/tutorial application showing how to rapidly find your nearest X by typing your postcode.", 186 | "updated": "2019-03-12T21:40:10Z" 187 | }, 188 | { 189 | "name": "learn-elm", 190 | "lang": "HTML", 191 | "url": "/dwyl/learn-elm", 192 | "description": "🌈 discover why people are switching to Elm and how you can get started today!", 193 | "updated": "2019-03-11T13:58:23Z" 194 | }, 195 | { 196 | "name": "technical-glossary", 197 | "lang": "", 198 | "url": "/dwyl/technical-glossary", 199 | "description": "📝 A technical glossary for key words and terms to help anyone learn and understand concepts and prepare for a career as a creative technologist! 😕 > 🤔 > 💡 > 😊 🎉 🚀", 200 | "updated": "2019-03-08T14:48:31Z" 201 | }, 202 | { 203 | "name": "learn-purescript", 204 | "lang": "", 205 | "url": "/dwyl/learn-purescript", 206 | "description": "🚧 Learn to use Purescript to make your JavaScript Apps more reliable.", 207 | "updated": "2019-03-08T14:35:47Z" 208 | }, 209 | { 210 | "name": "goodparts", 211 | "lang": "JavaScript", 212 | "url": "/dwyl/goodparts", 213 | "description": "🙈 An ESLint Style that only allows JavaScript the Good Parts (and \"Better Parts\") in your code.", 214 | "updated": "2019-03-06T14:45:23Z" 215 | }, 216 | { 217 | "name": "product-owner-guide", 218 | "lang": "", 219 | "url": "/dwyl/product-owner-guide", 220 | "description": "🚀 A rough guide for people working with dwyl as Product Owners", 221 | "updated": "2019-03-05T15:01:05Z" 222 | } 223 | ], 224 | "next_page": "/dwyl?page=2" 225 | } -------------------------------------------------------------------------------- /test/fixtures/person.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "/iteles", 3 | "type": "profile", 4 | "username": "iteles", 5 | "bio": "Co-founder @dwyl | Head cheerleader @foundersandcoders", 6 | "avatar": "https://avatars1.githubusercontent.com/u/4185328?s=400&v=4", 7 | "uid": 4185328, 8 | "repos": 28, 9 | "projects": 0, 10 | "stars": 453, 11 | "followers": 341, 12 | "following": 75, 13 | "pinned": [ 14 | { 15 | "url": "/dwyl/start-here" 16 | }, 17 | { 18 | "url": "/dwyl/learn-tdd" 19 | }, 20 | { 21 | "url": "/dwyl/learn-elm-architecture-in-javascript" 22 | }, 23 | { 24 | "url": "/dwyl/tachyons-bootstrap" 25 | }, 26 | { 27 | "url": "/dwyl/learn-ab-and-multivariate-testing" 28 | }, 29 | { 30 | "url": "/dwyl/learn-elixir" 31 | } 32 | ], 33 | "worksfor": "@dwyl", 34 | "location": "London, UK", 35 | "name": "Ines Teles Correia", 36 | "website": "https://www.twitter.com/iteles", 37 | "contribs": 869, 38 | "contrib_matrix": { 39 | "2018-04-15": { 40 | "fill": "#c6e48b", 41 | "count": 2, 42 | "x": "13", 43 | "y": "0" 44 | }, 45 | "2018-04-16": { 46 | "fill": "#c6e48b", 47 | "count": 1, 48 | "x": "13", 49 | "y": "12" 50 | }, 51 | "2018-04-17": { 52 | "fill": "#7bc96f", 53 | "count": 3, 54 | "x": "13", 55 | "y": "24" 56 | }, 57 | "2018-04-18": { 58 | "fill": "#c6e48b", 59 | "count": 2, 60 | "x": "13", 61 | "y": "36" 62 | }, 63 | "2018-04-19": { 64 | "fill": "#c6e48b", 65 | "count": 1, 66 | "x": "13", 67 | "y": "48" 68 | }, 69 | "2018-04-20": { 70 | "fill": "#ebedf0", 71 | "count": 0, 72 | "x": "13", 73 | "y": "60" 74 | }, 75 | "2018-04-21": { 76 | "fill": "#c6e48b", 77 | "count": 1, 78 | "x": "13", 79 | "y": "72" 80 | }, 81 | "2018-04-22": { 82 | "fill": "#ebedf0", 83 | "count": 0, 84 | "x": "12", 85 | "y": "0" 86 | }, 87 | "2018-04-23": { 88 | "fill": "#c6e48b", 89 | "count": 2, 90 | "x": "12", 91 | "y": "12" 92 | }, 93 | "2018-04-24": { 94 | "fill": "#7bc96f", 95 | "count": 4, 96 | "x": "12", 97 | "y": "24" 98 | }, 99 | "2018-04-25": { 100 | "fill": "#c6e48b", 101 | "count": 2, 102 | "x": "12", 103 | "y": "36" 104 | }, 105 | "2018-04-26": { 106 | "fill": "#239a3b", 107 | "count": 7, 108 | "x": "12", 109 | "y": "48" 110 | }, 111 | "2018-04-27": { 112 | "fill": "#c6e48b", 113 | "count": 2, 114 | "x": "12", 115 | "y": "60" 116 | }, 117 | "2018-04-28": { 118 | "fill": "#c6e48b", 119 | "count": 1, 120 | "x": "12", 121 | "y": "72" 122 | }, 123 | "2018-04-29": { 124 | "fill": "#7bc96f", 125 | "count": 3, 126 | "x": "11", 127 | "y": "0" 128 | }, 129 | "2018-04-30": { 130 | "fill": "#ebedf0", 131 | "count": 0, 132 | "x": "11", 133 | "y": "12" 134 | }, 135 | "2018-05-01": { 136 | "fill": "#7bc96f", 137 | "count": 3, 138 | "x": "11", 139 | "y": "24" 140 | }, 141 | "2018-05-02": { 142 | "fill": "#c6e48b", 143 | "count": 1, 144 | "x": "11", 145 | "y": "36" 146 | }, 147 | "2018-05-03": { 148 | "fill": "#196127", 149 | "count": 13, 150 | "x": "11", 151 | "y": "48" 152 | }, 153 | "2018-05-04": { 154 | "fill": "#ebedf0", 155 | "count": 0, 156 | "x": "11", 157 | "y": "60" 158 | }, 159 | "2018-05-05": { 160 | "fill": "#ebedf0", 161 | "count": 0, 162 | "x": "11", 163 | "y": "72" 164 | }, 165 | "2018-05-06": { 166 | "fill": "#ebedf0", 167 | "count": 0, 168 | "x": "10", 169 | "y": "0" 170 | }, 171 | "2018-05-07": { 172 | "fill": "#c6e48b", 173 | "count": 1, 174 | "x": "10", 175 | "y": "12" 176 | }, 177 | "2018-05-08": { 178 | "fill": "#c6e48b", 179 | "count": 1, 180 | "x": "10", 181 | "y": "24" 182 | }, 183 | "2018-05-09": { 184 | "fill": "#ebedf0", 185 | "count": 0, 186 | "x": "10", 187 | "y": "36" 188 | }, 189 | "2018-05-10": { 190 | "fill": "#7bc96f", 191 | "count": 3, 192 | "x": "10", 193 | "y": "48" 194 | }, 195 | "2018-05-11": { 196 | "fill": "#196127", 197 | "count": 15, 198 | "x": "10", 199 | "y": "60" 200 | }, 201 | "2018-05-12": { 202 | "fill": "#c6e48b", 203 | "count": 2, 204 | "x": "10", 205 | "y": "72" 206 | }, 207 | "2018-05-13": { 208 | "fill": "#c6e48b", 209 | "count": 1, 210 | "x": "9", 211 | "y": "0" 212 | }, 213 | "2018-05-14": { 214 | "fill": "#c6e48b", 215 | "count": 1, 216 | "x": "9", 217 | "y": "12" 218 | }, 219 | "2018-05-15": { 220 | "fill": "#c6e48b", 221 | "count": 2, 222 | "x": "9", 223 | "y": "24" 224 | }, 225 | "2018-05-16": { 226 | "fill": "#7bc96f", 227 | "count": 3, 228 | "x": "9", 229 | "y": "36" 230 | }, 231 | "2018-05-17": { 232 | "fill": "#196127", 233 | "count": 9, 234 | "x": "9", 235 | "y": "48" 236 | }, 237 | "2018-05-18": { 238 | "fill": "#7bc96f", 239 | "count": 3, 240 | "x": "9", 241 | "y": "60" 242 | }, 243 | "2018-05-19": { 244 | "fill": "#7bc96f", 245 | "count": 5, 246 | "x": "9", 247 | "y": "72" 248 | }, 249 | "2018-05-20": { 250 | "fill": "#c6e48b", 251 | "count": 2, 252 | "x": "8", 253 | "y": "0" 254 | }, 255 | "2018-05-21": { 256 | "fill": "#7bc96f", 257 | "count": 4, 258 | "x": "8", 259 | "y": "12" 260 | }, 261 | "2018-05-22": { 262 | "fill": "#7bc96f", 263 | "count": 3, 264 | "x": "8", 265 | "y": "24" 266 | }, 267 | "2018-05-23": { 268 | "fill": "#7bc96f", 269 | "count": 3, 270 | "x": "8", 271 | "y": "36" 272 | }, 273 | "2018-05-24": { 274 | "fill": "#7bc96f", 275 | "count": 3, 276 | "x": "8", 277 | "y": "48" 278 | }, 279 | "2018-05-25": { 280 | "fill": "#239a3b", 281 | "count": 8, 282 | "x": "8", 283 | "y": "60" 284 | }, 285 | "2018-05-26": { 286 | "fill": "#7bc96f", 287 | "count": 3, 288 | "x": "8", 289 | "y": "72" 290 | }, 291 | "2018-05-27": { 292 | "fill": "#ebedf0", 293 | "count": 0, 294 | "x": "7", 295 | "y": "0" 296 | }, 297 | "2018-05-28": { 298 | "fill": "#239a3b", 299 | "count": 6, 300 | "x": "7", 301 | "y": "12" 302 | }, 303 | "2018-05-29": { 304 | "fill": "#c6e48b", 305 | "count": 1, 306 | "x": "7", 307 | "y": "24" 308 | }, 309 | "2018-05-30": { 310 | "fill": "#7bc96f", 311 | "count": 4, 312 | "x": "7", 313 | "y": "36" 314 | }, 315 | "2018-05-31": { 316 | "fill": "#196127", 317 | "count": 10, 318 | "x": "7", 319 | "y": "48" 320 | }, 321 | "2018-06-01": { 322 | "fill": "#239a3b", 323 | "count": 6, 324 | "x": "7", 325 | "y": "60" 326 | }, 327 | "2018-06-02": { 328 | "fill": "#239a3b", 329 | "count": 6, 330 | "x": "7", 331 | "y": "72" 332 | }, 333 | "2018-06-03": { 334 | "fill": "#7bc96f", 335 | "count": 4, 336 | "x": "6", 337 | "y": "0" 338 | }, 339 | "2018-06-04": { 340 | "fill": "#239a3b", 341 | "count": 8, 342 | "x": "6", 343 | "y": "12" 344 | }, 345 | "2018-06-05": { 346 | "fill": "#7bc96f", 347 | "count": 3, 348 | "x": "6", 349 | "y": "24" 350 | }, 351 | "2018-06-06": { 352 | "fill": "#7bc96f", 353 | "count": 5, 354 | "x": "6", 355 | "y": "36" 356 | }, 357 | "2018-06-07": { 358 | "fill": "#ebedf0", 359 | "count": 0, 360 | "x": "6", 361 | "y": "48" 362 | }, 363 | "2018-06-08": { 364 | "fill": "#7bc96f", 365 | "count": 3, 366 | "x": "6", 367 | "y": "60" 368 | }, 369 | "2018-06-09": { 370 | "fill": "#c6e48b", 371 | "count": 2, 372 | "x": "6", 373 | "y": "72" 374 | }, 375 | "2018-06-10": { 376 | "fill": "#239a3b", 377 | "count": 6, 378 | "x": "5", 379 | "y": "0" 380 | }, 381 | "2018-06-11": { 382 | "fill": "#7bc96f", 383 | "count": 3, 384 | "x": "5", 385 | "y": "12" 386 | }, 387 | "2018-06-12": { 388 | "fill": "#7bc96f", 389 | "count": 3, 390 | "x": "5", 391 | "y": "24" 392 | }, 393 | "2018-06-13": { 394 | "fill": "#7bc96f", 395 | "count": 4, 396 | "x": "5", 397 | "y": "36" 398 | }, 399 | "2018-06-14": { 400 | "fill": "#c6e48b", 401 | "count": 1, 402 | "x": "5", 403 | "y": "48" 404 | }, 405 | "2018-06-15": { 406 | "fill": "#c6e48b", 407 | "count": 1, 408 | "x": "5", 409 | "y": "60" 410 | }, 411 | "2018-06-16": { 412 | "fill": "#c6e48b", 413 | "count": 2, 414 | "x": "5", 415 | "y": "72" 416 | }, 417 | "2018-06-17": { 418 | "fill": "#7bc96f", 419 | "count": 4, 420 | "x": "4", 421 | "y": "0" 422 | }, 423 | "2018-06-18": { 424 | "fill": "#ebedf0", 425 | "count": 0, 426 | "x": "4", 427 | "y": "12" 428 | }, 429 | "2018-06-19": { 430 | "fill": "#239a3b", 431 | "count": 7, 432 | "x": "4", 433 | "y": "24" 434 | }, 435 | "2018-06-20": { 436 | "fill": "#7bc96f", 437 | "count": 3, 438 | "x": "4", 439 | "y": "36" 440 | }, 441 | "2018-06-21": { 442 | "fill": "#7bc96f", 443 | "count": 5, 444 | "x": "4", 445 | "y": "48" 446 | }, 447 | "2018-06-22": { 448 | "fill": "#c6e48b", 449 | "count": 2, 450 | "x": "4", 451 | "y": "60" 452 | }, 453 | "2018-06-23": { 454 | "fill": "#ebedf0", 455 | "count": 0, 456 | "x": "4", 457 | "y": "72" 458 | }, 459 | "2018-06-24": { 460 | "fill": "#ebedf0", 461 | "count": 0, 462 | "x": "3", 463 | "y": "0" 464 | }, 465 | "2018-06-25": { 466 | "fill": "#7bc96f", 467 | "count": 4, 468 | "x": "3", 469 | "y": "12" 470 | }, 471 | "2018-06-26": { 472 | "fill": "#c6e48b", 473 | "count": 2, 474 | "x": "3", 475 | "y": "24" 476 | }, 477 | "2018-06-27": { 478 | "fill": "#c6e48b", 479 | "count": 2, 480 | "x": "3", 481 | "y": "36" 482 | }, 483 | "2018-06-28": { 484 | "fill": "#ebedf0", 485 | "count": 0, 486 | "x": "3", 487 | "y": "48" 488 | }, 489 | "2018-06-29": { 490 | "fill": "#c6e48b", 491 | "count": 1, 492 | "x": "3", 493 | "y": "60" 494 | }, 495 | "2018-06-30": { 496 | "fill": "#ebedf0", 497 | "count": 0, 498 | "x": "3", 499 | "y": "72" 500 | }, 501 | "2018-07-01": { 502 | "fill": "#ebedf0", 503 | "count": 0, 504 | "x": "2", 505 | "y": "0" 506 | }, 507 | "2018-07-02": { 508 | "fill": "#c6e48b", 509 | "count": 2, 510 | "x": "2", 511 | "y": "12" 512 | }, 513 | "2018-07-03": { 514 | "fill": "#ebedf0", 515 | "count": 0, 516 | "x": "2", 517 | "y": "24" 518 | }, 519 | "2018-07-04": { 520 | "fill": "#7bc96f", 521 | "count": 4, 522 | "x": "2", 523 | "y": "36" 524 | }, 525 | "2018-07-05": { 526 | "fill": "#c6e48b", 527 | "count": 1, 528 | "x": "2", 529 | "y": "48" 530 | }, 531 | "2018-07-06": { 532 | "fill": "#7bc96f", 533 | "count": 4, 534 | "x": "2", 535 | "y": "60" 536 | }, 537 | "2018-07-07": { 538 | "fill": "#ebedf0", 539 | "count": 0, 540 | "x": "2", 541 | "y": "72" 542 | }, 543 | "2018-07-08": { 544 | "fill": "#c6e48b", 545 | "count": 1, 546 | "x": "1", 547 | "y": "0" 548 | }, 549 | "2018-07-09": { 550 | "fill": "#c6e48b", 551 | "count": 1, 552 | "x": "1", 553 | "y": "12" 554 | }, 555 | "2018-07-10": { 556 | "fill": "#7bc96f", 557 | "count": 3, 558 | "x": "1", 559 | "y": "24" 560 | }, 561 | "2018-07-11": { 562 | "fill": "#c6e48b", 563 | "count": 1, 564 | "x": "1", 565 | "y": "36" 566 | }, 567 | "2018-07-12": { 568 | "fill": "#7bc96f", 569 | "count": 5, 570 | "x": "1", 571 | "y": "48" 572 | }, 573 | "2018-07-13": { 574 | "fill": "#7bc96f", 575 | "count": 4, 576 | "x": "1", 577 | "y": "60" 578 | }, 579 | "2018-07-14": { 580 | "fill": "#196127", 581 | "count": 9, 582 | "x": "1", 583 | "y": "72" 584 | }, 585 | "2018-07-15": { 586 | "fill": "#ebedf0", 587 | "count": 0, 588 | "x": "0", 589 | "y": "0" 590 | }, 591 | "2018-07-16": { 592 | "fill": "#7bc96f", 593 | "count": 4, 594 | "x": "0", 595 | "y": "12" 596 | }, 597 | "2018-07-17": { 598 | "fill": "#ebedf0", 599 | "count": 0, 600 | "x": "0", 601 | "y": "24" 602 | }, 603 | "2018-07-18": { 604 | "fill": "#ebedf0", 605 | "count": 0, 606 | "x": "0", 607 | "y": "36" 608 | }, 609 | "2018-07-19": { 610 | "fill": "#7bc96f", 611 | "count": 3, 612 | "x": "0", 613 | "y": "48" 614 | }, 615 | "2018-07-20": { 616 | "fill": "#ebedf0", 617 | "count": 0, 618 | "x": "0", 619 | "y": "60" 620 | }, 621 | "2018-07-21": { 622 | "fill": "#7bc96f", 623 | "count": 3, 624 | "x": "0", 625 | "y": "72" 626 | }, 627 | "2018-07-22": { 628 | "fill": "#ebedf0", 629 | "count": 0, 630 | "x": "-1", 631 | "y": "0" 632 | }, 633 | "2018-07-23": { 634 | "fill": "#c6e48b", 635 | "count": 2, 636 | "x": "-1", 637 | "y": "12" 638 | }, 639 | "2018-07-24": { 640 | "fill": "#c6e48b", 641 | "count": 2, 642 | "x": "-1", 643 | "y": "24" 644 | }, 645 | "2018-07-25": { 646 | "fill": "#c6e48b", 647 | "count": 1, 648 | "x": "-1", 649 | "y": "36" 650 | }, 651 | "2018-07-26": { 652 | "fill": "#239a3b", 653 | "count": 6, 654 | "x": "-1", 655 | "y": "48" 656 | }, 657 | "2018-07-27": { 658 | "fill": "#c6e48b", 659 | "count": 1, 660 | "x": "-1", 661 | "y": "60" 662 | }, 663 | "2018-07-28": { 664 | "fill": "#7bc96f", 665 | "count": 3, 666 | "x": "-1", 667 | "y": "72" 668 | }, 669 | "2018-07-29": { 670 | "fill": "#c6e48b", 671 | "count": 1, 672 | "x": "-2", 673 | "y": "0" 674 | }, 675 | "2018-07-30": { 676 | "fill": "#239a3b", 677 | "count": 7, 678 | "x": "-2", 679 | "y": "12" 680 | }, 681 | "2018-07-31": { 682 | "fill": "#c6e48b", 683 | "count": 1, 684 | "x": "-2", 685 | "y": "24" 686 | }, 687 | "2018-08-01": { 688 | "fill": "#239a3b", 689 | "count": 6, 690 | "x": "-2", 691 | "y": "36" 692 | }, 693 | "2018-08-02": { 694 | "fill": "#239a3b", 695 | "count": 6, 696 | "x": "-2", 697 | "y": "48" 698 | }, 699 | "2018-08-03": { 700 | "fill": "#c6e48b", 701 | "count": 2, 702 | "x": "-2", 703 | "y": "60" 704 | }, 705 | "2018-08-04": { 706 | "fill": "#c6e48b", 707 | "count": 1, 708 | "x": "-2", 709 | "y": "72" 710 | }, 711 | "2018-08-05": { 712 | "fill": "#ebedf0", 713 | "count": 0, 714 | "x": "-3", 715 | "y": "0" 716 | }, 717 | "2018-08-06": { 718 | "fill": "#ebedf0", 719 | "count": 0, 720 | "x": "-3", 721 | "y": "12" 722 | }, 723 | "2018-08-07": { 724 | "fill": "#c6e48b", 725 | "count": 2, 726 | "x": "-3", 727 | "y": "24" 728 | }, 729 | "2018-08-08": { 730 | "fill": "#c6e48b", 731 | "count": 1, 732 | "x": "-3", 733 | "y": "36" 734 | }, 735 | "2018-08-09": { 736 | "fill": "#7bc96f", 737 | "count": 5, 738 | "x": "-3", 739 | "y": "48" 740 | }, 741 | "2018-08-10": { 742 | "fill": "#239a3b", 743 | "count": 6, 744 | "x": "-3", 745 | "y": "60" 746 | }, 747 | "2018-08-11": { 748 | "fill": "#c6e48b", 749 | "count": 1, 750 | "x": "-3", 751 | "y": "72" 752 | }, 753 | "2018-08-12": { 754 | "fill": "#7bc96f", 755 | "count": 5, 756 | "x": "-4", 757 | "y": "0" 758 | }, 759 | "2018-08-13": { 760 | "fill": "#7bc96f", 761 | "count": 4, 762 | "x": "-4", 763 | "y": "12" 764 | }, 765 | "2018-08-14": { 766 | "fill": "#239a3b", 767 | "count": 7, 768 | "x": "-4", 769 | "y": "24" 770 | }, 771 | "2018-08-15": { 772 | "fill": "#c6e48b", 773 | "count": 2, 774 | "x": "-4", 775 | "y": "36" 776 | }, 777 | "2018-08-16": { 778 | "fill": "#c6e48b", 779 | "count": 2, 780 | "x": "-4", 781 | "y": "48" 782 | }, 783 | "2018-08-17": { 784 | "fill": "#c6e48b", 785 | "count": 1, 786 | "x": "-4", 787 | "y": "60" 788 | }, 789 | "2018-08-18": { 790 | "fill": "#c6e48b", 791 | "count": 1, 792 | "x": "-4", 793 | "y": "72" 794 | }, 795 | "2018-08-19": { 796 | "fill": "#ebedf0", 797 | "count": 0, 798 | "x": "-5", 799 | "y": "0" 800 | }, 801 | "2018-08-20": { 802 | "fill": "#ebedf0", 803 | "count": 0, 804 | "x": "-5", 805 | "y": "12" 806 | }, 807 | "2018-08-21": { 808 | "fill": "#ebedf0", 809 | "count": 0, 810 | "x": "-5", 811 | "y": "24" 812 | }, 813 | "2018-08-22": { 814 | "fill": "#ebedf0", 815 | "count": 0, 816 | "x": "-5", 817 | "y": "36" 818 | }, 819 | "2018-08-23": { 820 | "fill": "#7bc96f", 821 | "count": 4, 822 | "x": "-5", 823 | "y": "48" 824 | }, 825 | "2018-08-24": { 826 | "fill": "#7bc96f", 827 | "count": 3, 828 | "x": "-5", 829 | "y": "60" 830 | }, 831 | "2018-08-25": { 832 | "fill": "#ebedf0", 833 | "count": 0, 834 | "x": "-5", 835 | "y": "72" 836 | }, 837 | "2018-08-26": { 838 | "fill": "#ebedf0", 839 | "count": 0, 840 | "x": "-6", 841 | "y": "0" 842 | }, 843 | "2018-08-27": { 844 | "fill": "#c6e48b", 845 | "count": 1, 846 | "x": "-6", 847 | "y": "12" 848 | }, 849 | "2018-08-28": { 850 | "fill": "#ebedf0", 851 | "count": 0, 852 | "x": "-6", 853 | "y": "24" 854 | }, 855 | "2018-08-29": { 856 | "fill": "#c6e48b", 857 | "count": 1, 858 | "x": "-6", 859 | "y": "36" 860 | }, 861 | "2018-08-30": { 862 | "fill": "#ebedf0", 863 | "count": 0, 864 | "x": "-6", 865 | "y": "48" 866 | }, 867 | "2018-08-31": { 868 | "fill": "#ebedf0", 869 | "count": 0, 870 | "x": "-6", 871 | "y": "60" 872 | }, 873 | "2018-09-01": { 874 | "fill": "#239a3b", 875 | "count": 8, 876 | "x": "-6", 877 | "y": "72" 878 | }, 879 | "2018-09-02": { 880 | "fill": "#ebedf0", 881 | "count": 0, 882 | "x": "-7", 883 | "y": "0" 884 | }, 885 | "2018-09-03": { 886 | "fill": "#7bc96f", 887 | "count": 4, 888 | "x": "-7", 889 | "y": "12" 890 | }, 891 | "2018-09-04": { 892 | "fill": "#7bc96f", 893 | "count": 4, 894 | "x": "-7", 895 | "y": "24" 896 | }, 897 | "2018-09-05": { 898 | "fill": "#7bc96f", 899 | "count": 4, 900 | "x": "-7", 901 | "y": "36" 902 | }, 903 | "2018-09-06": { 904 | "fill": "#196127", 905 | "count": 9, 906 | "x": "-7", 907 | "y": "48" 908 | }, 909 | "2018-09-07": { 910 | "fill": "#c6e48b", 911 | "count": 2, 912 | "x": "-7", 913 | "y": "60" 914 | }, 915 | "2018-09-08": { 916 | "fill": "#7bc96f", 917 | "count": 5, 918 | "x": "-7", 919 | "y": "72" 920 | }, 921 | "2018-09-09": { 922 | "fill": "#ebedf0", 923 | "count": 0, 924 | "x": "-8", 925 | "y": "0" 926 | }, 927 | "2018-09-10": { 928 | "fill": "#c6e48b", 929 | "count": 2, 930 | "x": "-8", 931 | "y": "12" 932 | }, 933 | "2018-09-11": { 934 | "fill": "#c6e48b", 935 | "count": 2, 936 | "x": "-8", 937 | "y": "24" 938 | }, 939 | "2018-09-12": { 940 | "fill": "#7bc96f", 941 | "count": 4, 942 | "x": "-8", 943 | "y": "36" 944 | }, 945 | "2018-09-13": { 946 | "fill": "#c6e48b", 947 | "count": 1, 948 | "x": "-8", 949 | "y": "48" 950 | }, 951 | "2018-09-14": { 952 | "fill": "#c6e48b", 953 | "count": 1, 954 | "x": "-8", 955 | "y": "60" 956 | }, 957 | "2018-09-15": { 958 | "fill": "#ebedf0", 959 | "count": 0, 960 | "x": "-8", 961 | "y": "72" 962 | }, 963 | "2018-09-16": { 964 | "fill": "#ebedf0", 965 | "count": 0, 966 | "x": "-9", 967 | "y": "0" 968 | }, 969 | "2018-09-17": { 970 | "fill": "#ebedf0", 971 | "count": 0, 972 | "x": "-9", 973 | "y": "12" 974 | }, 975 | "2018-09-18": { 976 | "fill": "#196127", 977 | "count": 9, 978 | "x": "-9", 979 | "y": "24" 980 | }, 981 | "2018-09-19": { 982 | "fill": "#ebedf0", 983 | "count": 0, 984 | "x": "-9", 985 | "y": "36" 986 | }, 987 | "2018-09-20": { 988 | "fill": "#ebedf0", 989 | "count": 0, 990 | "x": "-9", 991 | "y": "48" 992 | }, 993 | "2018-09-21": { 994 | "fill": "#c6e48b", 995 | "count": 2, 996 | "x": "-9", 997 | "y": "60" 998 | }, 999 | "2018-09-22": { 1000 | "fill": "#ebedf0", 1001 | "count": 0, 1002 | "x": "-9", 1003 | "y": "72" 1004 | }, 1005 | "2018-09-23": { 1006 | "fill": "#c6e48b", 1007 | "count": 2, 1008 | "x": "-10", 1009 | "y": "0" 1010 | }, 1011 | "2018-09-24": { 1012 | "fill": "#c6e48b", 1013 | "count": 1, 1014 | "x": "-10", 1015 | "y": "12" 1016 | }, 1017 | "2018-09-25": { 1018 | "fill": "#c6e48b", 1019 | "count": 1, 1020 | "x": "-10", 1021 | "y": "24" 1022 | }, 1023 | "2018-09-26": { 1024 | "fill": "#7bc96f", 1025 | "count": 4, 1026 | "x": "-10", 1027 | "y": "36" 1028 | }, 1029 | "2018-09-27": { 1030 | "fill": "#239a3b", 1031 | "count": 7, 1032 | "x": "-10", 1033 | "y": "48" 1034 | }, 1035 | "2018-09-28": { 1036 | "fill": "#196127", 1037 | "count": 9, 1038 | "x": "-10", 1039 | "y": "60" 1040 | }, 1041 | "2018-09-29": { 1042 | "fill": "#c6e48b", 1043 | "count": 2, 1044 | "x": "-10", 1045 | "y": "72" 1046 | }, 1047 | "2018-09-30": { 1048 | "fill": "#7bc96f", 1049 | "count": 5, 1050 | "x": "-11", 1051 | "y": "0" 1052 | }, 1053 | "2018-10-01": { 1054 | "fill": "#ebedf0", 1055 | "count": 0, 1056 | "x": "-11", 1057 | "y": "12" 1058 | }, 1059 | "2018-10-02": { 1060 | "fill": "#c6e48b", 1061 | "count": 1, 1062 | "x": "-11", 1063 | "y": "24" 1064 | }, 1065 | "2018-10-03": { 1066 | "fill": "#c6e48b", 1067 | "count": 2, 1068 | "x": "-11", 1069 | "y": "36" 1070 | }, 1071 | "2018-10-04": { 1072 | "fill": "#c6e48b", 1073 | "count": 2, 1074 | "x": "-11", 1075 | "y": "48" 1076 | }, 1077 | "2018-10-05": { 1078 | "fill": "#7bc96f", 1079 | "count": 3, 1080 | "x": "-11", 1081 | "y": "60" 1082 | }, 1083 | "2018-10-06": { 1084 | "fill": "#c6e48b", 1085 | "count": 2, 1086 | "x": "-11", 1087 | "y": "72" 1088 | }, 1089 | "2018-10-07": { 1090 | "fill": "#c6e48b", 1091 | "count": 1, 1092 | "x": "-12", 1093 | "y": "0" 1094 | }, 1095 | "2018-10-08": { 1096 | "fill": "#ebedf0", 1097 | "count": 0, 1098 | "x": "-12", 1099 | "y": "12" 1100 | }, 1101 | "2018-10-09": { 1102 | "fill": "#7bc96f", 1103 | "count": 3, 1104 | "x": "-12", 1105 | "y": "24" 1106 | }, 1107 | "2018-10-10": { 1108 | "fill": "#c6e48b", 1109 | "count": 2, 1110 | "x": "-12", 1111 | "y": "36" 1112 | }, 1113 | "2018-10-11": { 1114 | "fill": "#c6e48b", 1115 | "count": 2, 1116 | "x": "-12", 1117 | "y": "48" 1118 | }, 1119 | "2018-10-12": { 1120 | "fill": "#7bc96f", 1121 | "count": 4, 1122 | "x": "-12", 1123 | "y": "60" 1124 | }, 1125 | "2018-10-13": { 1126 | "fill": "#c6e48b", 1127 | "count": 1, 1128 | "x": "-12", 1129 | "y": "72" 1130 | }, 1131 | "2018-10-14": { 1132 | "fill": "#ebedf0", 1133 | "count": 0, 1134 | "x": "-13", 1135 | "y": "0" 1136 | }, 1137 | "2018-10-15": { 1138 | "fill": "#196127", 1139 | "count": 9, 1140 | "x": "-13", 1141 | "y": "12" 1142 | }, 1143 | "2018-10-16": { 1144 | "fill": "#7bc96f", 1145 | "count": 3, 1146 | "x": "-13", 1147 | "y": "24" 1148 | }, 1149 | "2018-10-17": { 1150 | "fill": "#7bc96f", 1151 | "count": 4, 1152 | "x": "-13", 1153 | "y": "36" 1154 | }, 1155 | "2018-10-18": { 1156 | "fill": "#c6e48b", 1157 | "count": 2, 1158 | "x": "-13", 1159 | "y": "48" 1160 | }, 1161 | "2018-10-19": { 1162 | "fill": "#c6e48b", 1163 | "count": 1, 1164 | "x": "-13", 1165 | "y": "60" 1166 | }, 1167 | "2018-10-20": { 1168 | "fill": "#c6e48b", 1169 | "count": 1, 1170 | "x": "-13", 1171 | "y": "72" 1172 | }, 1173 | "2018-10-21": { 1174 | "fill": "#7bc96f", 1175 | "count": 4, 1176 | "x": "-14", 1177 | "y": "0" 1178 | }, 1179 | "2018-10-22": { 1180 | "fill": "#239a3b", 1181 | "count": 8, 1182 | "x": "-14", 1183 | "y": "12" 1184 | }, 1185 | "2018-10-23": { 1186 | "fill": "#7bc96f", 1187 | "count": 4, 1188 | "x": "-14", 1189 | "y": "24" 1190 | }, 1191 | "2018-10-24": { 1192 | "fill": "#239a3b", 1193 | "count": 6, 1194 | "x": "-14", 1195 | "y": "36" 1196 | }, 1197 | "2018-10-25": { 1198 | "fill": "#239a3b", 1199 | "count": 6, 1200 | "x": "-14", 1201 | "y": "48" 1202 | }, 1203 | "2018-10-26": { 1204 | "fill": "#c6e48b", 1205 | "count": 1, 1206 | "x": "-14", 1207 | "y": "60" 1208 | }, 1209 | "2018-10-27": { 1210 | "fill": "#c6e48b", 1211 | "count": 2, 1212 | "x": "-14", 1213 | "y": "72" 1214 | }, 1215 | "2018-10-28": { 1216 | "fill": "#c6e48b", 1217 | "count": 1, 1218 | "x": "-15", 1219 | "y": "0" 1220 | }, 1221 | "2018-10-29": { 1222 | "fill": "#196127", 1223 | "count": 16, 1224 | "x": "-15", 1225 | "y": "12" 1226 | }, 1227 | "2018-10-30": { 1228 | "fill": "#c6e48b", 1229 | "count": 2, 1230 | "x": "-15", 1231 | "y": "24" 1232 | }, 1233 | "2018-10-31": { 1234 | "fill": "#c6e48b", 1235 | "count": 2, 1236 | "x": "-15", 1237 | "y": "36" 1238 | }, 1239 | "2018-11-01": { 1240 | "fill": "#7bc96f", 1241 | "count": 5, 1242 | "x": "-15", 1243 | "y": "48" 1244 | }, 1245 | "2018-11-02": { 1246 | "fill": "#ebedf0", 1247 | "count": 0, 1248 | "x": "-15", 1249 | "y": "60" 1250 | }, 1251 | "2018-11-03": { 1252 | "fill": "#7bc96f", 1253 | "count": 4, 1254 | "x": "-15", 1255 | "y": "72" 1256 | }, 1257 | "2018-11-04": { 1258 | "fill": "#c6e48b", 1259 | "count": 2, 1260 | "x": "-16", 1261 | "y": "0" 1262 | }, 1263 | "2018-11-05": { 1264 | "fill": "#ebedf0", 1265 | "count": 0, 1266 | "x": "-16", 1267 | "y": "12" 1268 | }, 1269 | "2018-11-06": { 1270 | "fill": "#c6e48b", 1271 | "count": 2, 1272 | "x": "-16", 1273 | "y": "24" 1274 | }, 1275 | "2018-11-07": { 1276 | "fill": "#7bc96f", 1277 | "count": 3, 1278 | "x": "-16", 1279 | "y": "36" 1280 | }, 1281 | "2018-11-08": { 1282 | "fill": "#c6e48b", 1283 | "count": 2, 1284 | "x": "-16", 1285 | "y": "48" 1286 | }, 1287 | "2018-11-09": { 1288 | "fill": "#7bc96f", 1289 | "count": 3, 1290 | "x": "-16", 1291 | "y": "60" 1292 | }, 1293 | "2018-11-10": { 1294 | "fill": "#c6e48b", 1295 | "count": 2, 1296 | "x": "-16", 1297 | "y": "72" 1298 | }, 1299 | "2018-11-11": { 1300 | "fill": "#7bc96f", 1301 | "count": 3, 1302 | "x": "-17", 1303 | "y": "0" 1304 | }, 1305 | "2018-11-12": { 1306 | "fill": "#c6e48b", 1307 | "count": 1, 1308 | "x": "-17", 1309 | "y": "12" 1310 | }, 1311 | "2018-11-13": { 1312 | "fill": "#c6e48b", 1313 | "count": 1, 1314 | "x": "-17", 1315 | "y": "24" 1316 | }, 1317 | "2018-11-14": { 1318 | "fill": "#239a3b", 1319 | "count": 8, 1320 | "x": "-17", 1321 | "y": "36" 1322 | }, 1323 | "2018-11-15": { 1324 | "fill": "#7bc96f", 1325 | "count": 4, 1326 | "x": "-17", 1327 | "y": "48" 1328 | }, 1329 | "2018-11-16": { 1330 | "fill": "#7bc96f", 1331 | "count": 3, 1332 | "x": "-17", 1333 | "y": "60" 1334 | }, 1335 | "2018-11-17": { 1336 | "fill": "#7bc96f", 1337 | "count": 4, 1338 | "x": "-17", 1339 | "y": "72" 1340 | }, 1341 | "2018-11-18": { 1342 | "fill": "#ebedf0", 1343 | "count": 0, 1344 | "x": "-18", 1345 | "y": "0" 1346 | }, 1347 | "2018-11-19": { 1348 | "fill": "#7bc96f", 1349 | "count": 3, 1350 | "x": "-18", 1351 | "y": "12" 1352 | }, 1353 | "2018-11-20": { 1354 | "fill": "#7bc96f", 1355 | "count": 3, 1356 | "x": "-18", 1357 | "y": "24" 1358 | }, 1359 | "2018-11-21": { 1360 | "fill": "#ebedf0", 1361 | "count": 0, 1362 | "x": "-18", 1363 | "y": "36" 1364 | }, 1365 | "2018-11-22": { 1366 | "fill": "#c6e48b", 1367 | "count": 2, 1368 | "x": "-18", 1369 | "y": "48" 1370 | }, 1371 | "2018-11-23": { 1372 | "fill": "#7bc96f", 1373 | "count": 3, 1374 | "x": "-18", 1375 | "y": "60" 1376 | }, 1377 | "2018-11-24": { 1378 | "fill": "#7bc96f", 1379 | "count": 3, 1380 | "x": "-18", 1381 | "y": "72" 1382 | }, 1383 | "2018-11-25": { 1384 | "fill": "#c6e48b", 1385 | "count": 2, 1386 | "x": "-19", 1387 | "y": "0" 1388 | }, 1389 | "2018-11-26": { 1390 | "fill": "#7bc96f", 1391 | "count": 3, 1392 | "x": "-19", 1393 | "y": "12" 1394 | }, 1395 | "2018-11-27": { 1396 | "fill": "#ebedf0", 1397 | "count": 0, 1398 | "x": "-19", 1399 | "y": "24" 1400 | }, 1401 | "2018-11-28": { 1402 | "fill": "#7bc96f", 1403 | "count": 5, 1404 | "x": "-19", 1405 | "y": "36" 1406 | }, 1407 | "2018-11-29": { 1408 | "fill": "#196127", 1409 | "count": 9, 1410 | "x": "-19", 1411 | "y": "48" 1412 | }, 1413 | "2018-11-30": { 1414 | "fill": "#c6e48b", 1415 | "count": 1, 1416 | "x": "-19", 1417 | "y": "60" 1418 | }, 1419 | "2018-12-01": { 1420 | "fill": "#c6e48b", 1421 | "count": 2, 1422 | "x": "-19", 1423 | "y": "72" 1424 | }, 1425 | "2018-12-02": { 1426 | "fill": "#7bc96f", 1427 | "count": 5, 1428 | "x": "-20", 1429 | "y": "0" 1430 | }, 1431 | "2018-12-03": { 1432 | "fill": "#c6e48b", 1433 | "count": 2, 1434 | "x": "-20", 1435 | "y": "12" 1436 | }, 1437 | "2018-12-04": { 1438 | "fill": "#239a3b", 1439 | "count": 6, 1440 | "x": "-20", 1441 | "y": "24" 1442 | }, 1443 | "2018-12-05": { 1444 | "fill": "#7bc96f", 1445 | "count": 3, 1446 | "x": "-20", 1447 | "y": "36" 1448 | }, 1449 | "2018-12-06": { 1450 | "fill": "#7bc96f", 1451 | "count": 4, 1452 | "x": "-20", 1453 | "y": "48" 1454 | }, 1455 | "2018-12-07": { 1456 | "fill": "#c6e48b", 1457 | "count": 1, 1458 | "x": "-20", 1459 | "y": "60" 1460 | }, 1461 | "2018-12-08": { 1462 | "fill": "#7bc96f", 1463 | "count": 4, 1464 | "x": "-20", 1465 | "y": "72" 1466 | }, 1467 | "2018-12-09": { 1468 | "fill": "#ebedf0", 1469 | "count": 0, 1470 | "x": "-21", 1471 | "y": "0" 1472 | }, 1473 | "2018-12-10": { 1474 | "fill": "#7bc96f", 1475 | "count": 3, 1476 | "x": "-21", 1477 | "y": "12" 1478 | }, 1479 | "2018-12-11": { 1480 | "fill": "#239a3b", 1481 | "count": 6, 1482 | "x": "-21", 1483 | "y": "24" 1484 | }, 1485 | "2018-12-12": { 1486 | "fill": "#c6e48b", 1487 | "count": 1, 1488 | "x": "-21", 1489 | "y": "36" 1490 | }, 1491 | "2018-12-13": { 1492 | "fill": "#c6e48b", 1493 | "count": 2, 1494 | "x": "-21", 1495 | "y": "48" 1496 | }, 1497 | "2018-12-14": { 1498 | "fill": "#7bc96f", 1499 | "count": 3, 1500 | "x": "-21", 1501 | "y": "60" 1502 | }, 1503 | "2018-12-15": { 1504 | "fill": "#ebedf0", 1505 | "count": 0, 1506 | "x": "-21", 1507 | "y": "72" 1508 | }, 1509 | "2018-12-16": { 1510 | "fill": "#c6e48b", 1511 | "count": 1, 1512 | "x": "-22", 1513 | "y": "0" 1514 | }, 1515 | "2018-12-17": { 1516 | "fill": "#ebedf0", 1517 | "count": 0, 1518 | "x": "-22", 1519 | "y": "12" 1520 | }, 1521 | "2018-12-18": { 1522 | "fill": "#ebedf0", 1523 | "count": 0, 1524 | "x": "-22", 1525 | "y": "24" 1526 | }, 1527 | "2018-12-19": { 1528 | "fill": "#ebedf0", 1529 | "count": 0, 1530 | "x": "-22", 1531 | "y": "36" 1532 | }, 1533 | "2018-12-20": { 1534 | "fill": "#ebedf0", 1535 | "count": 0, 1536 | "x": "-22", 1537 | "y": "48" 1538 | }, 1539 | "2018-12-21": { 1540 | "fill": "#ebedf0", 1541 | "count": 0, 1542 | "x": "-22", 1543 | "y": "60" 1544 | }, 1545 | "2018-12-22": { 1546 | "fill": "#ebedf0", 1547 | "count": 0, 1548 | "x": "-22", 1549 | "y": "72" 1550 | }, 1551 | "2018-12-23": { 1552 | "fill": "#ebedf0", 1553 | "count": 0, 1554 | "x": "-23", 1555 | "y": "0" 1556 | }, 1557 | "2018-12-24": { 1558 | "fill": "#ebedf0", 1559 | "count": 0, 1560 | "x": "-23", 1561 | "y": "12" 1562 | }, 1563 | "2018-12-25": { 1564 | "fill": "#ebedf0", 1565 | "count": 0, 1566 | "x": "-23", 1567 | "y": "24" 1568 | }, 1569 | "2018-12-26": { 1570 | "fill": "#ebedf0", 1571 | "count": 0, 1572 | "x": "-23", 1573 | "y": "36" 1574 | }, 1575 | "2018-12-27": { 1576 | "fill": "#ebedf0", 1577 | "count": 0, 1578 | "x": "-23", 1579 | "y": "48" 1580 | }, 1581 | "2018-12-28": { 1582 | "fill": "#ebedf0", 1583 | "count": 0, 1584 | "x": "-23", 1585 | "y": "60" 1586 | }, 1587 | "2018-12-29": { 1588 | "fill": "#ebedf0", 1589 | "count": 0, 1590 | "x": "-23", 1591 | "y": "72" 1592 | }, 1593 | "2018-12-30": { 1594 | "fill": "#ebedf0", 1595 | "count": 0, 1596 | "x": "-24", 1597 | "y": "0" 1598 | }, 1599 | "2018-12-31": { 1600 | "fill": "#ebedf0", 1601 | "count": 0, 1602 | "x": "-24", 1603 | "y": "12" 1604 | }, 1605 | "2019-01-01": { 1606 | "fill": "#7bc96f", 1607 | "count": 3, 1608 | "x": "-24", 1609 | "y": "24" 1610 | }, 1611 | "2019-01-02": { 1612 | "fill": "#7bc96f", 1613 | "count": 4, 1614 | "x": "-24", 1615 | "y": "36" 1616 | }, 1617 | "2019-01-03": { 1618 | "fill": "#c6e48b", 1619 | "count": 2, 1620 | "x": "-24", 1621 | "y": "48" 1622 | }, 1623 | "2019-01-04": { 1624 | "fill": "#7bc96f", 1625 | "count": 5, 1626 | "x": "-24", 1627 | "y": "60" 1628 | }, 1629 | "2019-01-05": { 1630 | "fill": "#c6e48b", 1631 | "count": 1, 1632 | "x": "-24", 1633 | "y": "72" 1634 | }, 1635 | "2019-01-06": { 1636 | "fill": "#c6e48b", 1637 | "count": 1, 1638 | "x": "-25", 1639 | "y": "0" 1640 | }, 1641 | "2019-01-07": { 1642 | "fill": "#c6e48b", 1643 | "count": 1, 1644 | "x": "-25", 1645 | "y": "12" 1646 | }, 1647 | "2019-01-08": { 1648 | "fill": "#7bc96f", 1649 | "count": 5, 1650 | "x": "-25", 1651 | "y": "24" 1652 | }, 1653 | "2019-01-09": { 1654 | "fill": "#c6e48b", 1655 | "count": 1, 1656 | "x": "-25", 1657 | "y": "36" 1658 | }, 1659 | "2019-01-10": { 1660 | "fill": "#c6e48b", 1661 | "count": 1, 1662 | "x": "-25", 1663 | "y": "48" 1664 | }, 1665 | "2019-01-11": { 1666 | "fill": "#ebedf0", 1667 | "count": 0, 1668 | "x": "-25", 1669 | "y": "60" 1670 | }, 1671 | "2019-01-12": { 1672 | "fill": "#c6e48b", 1673 | "count": 1, 1674 | "x": "-25", 1675 | "y": "72" 1676 | }, 1677 | "2019-01-13": { 1678 | "fill": "#c6e48b", 1679 | "count": 1, 1680 | "x": "-26", 1681 | "y": "0" 1682 | }, 1683 | "2019-01-14": { 1684 | "fill": "#ebedf0", 1685 | "count": 0, 1686 | "x": "-26", 1687 | "y": "12" 1688 | }, 1689 | "2019-01-15": { 1690 | "fill": "#c6e48b", 1691 | "count": 2, 1692 | "x": "-26", 1693 | "y": "24" 1694 | }, 1695 | "2019-01-16": { 1696 | "fill": "#c6e48b", 1697 | "count": 2, 1698 | "x": "-26", 1699 | "y": "36" 1700 | }, 1701 | "2019-01-17": { 1702 | "fill": "#ebedf0", 1703 | "count": 0, 1704 | "x": "-26", 1705 | "y": "48" 1706 | }, 1707 | "2019-01-18": { 1708 | "fill": "#7bc96f", 1709 | "count": 3, 1710 | "x": "-26", 1711 | "y": "60" 1712 | }, 1713 | "2019-01-19": { 1714 | "fill": "#c6e48b", 1715 | "count": 2, 1716 | "x": "-26", 1717 | "y": "72" 1718 | }, 1719 | "2019-01-20": { 1720 | "fill": "#c6e48b", 1721 | "count": 1, 1722 | "x": "-27", 1723 | "y": "0" 1724 | }, 1725 | "2019-01-21": { 1726 | "fill": "#c6e48b", 1727 | "count": 1, 1728 | "x": "-27", 1729 | "y": "12" 1730 | }, 1731 | "2019-01-22": { 1732 | "fill": "#7bc96f", 1733 | "count": 4, 1734 | "x": "-27", 1735 | "y": "24" 1736 | }, 1737 | "2019-01-23": { 1738 | "fill": "#7bc96f", 1739 | "count": 5, 1740 | "x": "-27", 1741 | "y": "36" 1742 | }, 1743 | "2019-01-24": { 1744 | "fill": "#7bc96f", 1745 | "count": 3, 1746 | "x": "-27", 1747 | "y": "48" 1748 | }, 1749 | "2019-01-25": { 1750 | "fill": "#c6e48b", 1751 | "count": 2, 1752 | "x": "-27", 1753 | "y": "60" 1754 | }, 1755 | "2019-01-26": { 1756 | "fill": "#c6e48b", 1757 | "count": 2, 1758 | "x": "-27", 1759 | "y": "72" 1760 | }, 1761 | "2019-01-27": { 1762 | "fill": "#ebedf0", 1763 | "count": 0, 1764 | "x": "-28", 1765 | "y": "0" 1766 | }, 1767 | "2019-01-28": { 1768 | "fill": "#7bc96f", 1769 | "count": 3, 1770 | "x": "-28", 1771 | "y": "12" 1772 | }, 1773 | "2019-01-29": { 1774 | "fill": "#c6e48b", 1775 | "count": 2, 1776 | "x": "-28", 1777 | "y": "24" 1778 | }, 1779 | "2019-01-30": { 1780 | "fill": "#c6e48b", 1781 | "count": 2, 1782 | "x": "-28", 1783 | "y": "36" 1784 | }, 1785 | "2019-01-31": { 1786 | "fill": "#7bc96f", 1787 | "count": 5, 1788 | "x": "-28", 1789 | "y": "48" 1790 | }, 1791 | "2019-02-01": { 1792 | "fill": "#ebedf0", 1793 | "count": 0, 1794 | "x": "-28", 1795 | "y": "60" 1796 | }, 1797 | "2019-02-02": { 1798 | "fill": "#c6e48b", 1799 | "count": 1, 1800 | "x": "-28", 1801 | "y": "72" 1802 | }, 1803 | "2019-02-03": { 1804 | "fill": "#ebedf0", 1805 | "count": 0, 1806 | "x": "-29", 1807 | "y": "0" 1808 | }, 1809 | "2019-02-04": { 1810 | "fill": "#c6e48b", 1811 | "count": 1, 1812 | "x": "-29", 1813 | "y": "12" 1814 | }, 1815 | "2019-02-05": { 1816 | "fill": "#239a3b", 1817 | "count": 7, 1818 | "x": "-29", 1819 | "y": "24" 1820 | }, 1821 | "2019-02-06": { 1822 | "fill": "#c6e48b", 1823 | "count": 1, 1824 | "x": "-29", 1825 | "y": "36" 1826 | }, 1827 | "2019-02-07": { 1828 | "fill": "#c6e48b", 1829 | "count": 2, 1830 | "x": "-29", 1831 | "y": "48" 1832 | }, 1833 | "2019-02-08": { 1834 | "fill": "#c6e48b", 1835 | "count": 2, 1836 | "x": "-29", 1837 | "y": "60" 1838 | }, 1839 | "2019-02-09": { 1840 | "fill": "#c6e48b", 1841 | "count": 1, 1842 | "x": "-29", 1843 | "y": "72" 1844 | }, 1845 | "2019-02-10": { 1846 | "fill": "#c6e48b", 1847 | "count": 2, 1848 | "x": "-30", 1849 | "y": "0" 1850 | }, 1851 | "2019-02-11": { 1852 | "fill": "#239a3b", 1853 | "count": 8, 1854 | "x": "-30", 1855 | "y": "12" 1856 | }, 1857 | "2019-02-12": { 1858 | "fill": "#7bc96f", 1859 | "count": 4, 1860 | "x": "-30", 1861 | "y": "24" 1862 | }, 1863 | "2019-02-13": { 1864 | "fill": "#ebedf0", 1865 | "count": 0, 1866 | "x": "-30", 1867 | "y": "36" 1868 | }, 1869 | "2019-02-14": { 1870 | "fill": "#239a3b", 1871 | "count": 6, 1872 | "x": "-30", 1873 | "y": "48" 1874 | }, 1875 | "2019-02-15": { 1876 | "fill": "#c6e48b", 1877 | "count": 1, 1878 | "x": "-30", 1879 | "y": "60" 1880 | }, 1881 | "2019-02-16": { 1882 | "fill": "#c6e48b", 1883 | "count": 2, 1884 | "x": "-30", 1885 | "y": "72" 1886 | }, 1887 | "2019-02-17": { 1888 | "fill": "#c6e48b", 1889 | "count": 2, 1890 | "x": "-31", 1891 | "y": "0" 1892 | }, 1893 | "2019-02-18": { 1894 | "fill": "#ebedf0", 1895 | "count": 0, 1896 | "x": "-31", 1897 | "y": "12" 1898 | }, 1899 | "2019-02-19": { 1900 | "fill": "#196127", 1901 | "count": 10, 1902 | "x": "-31", 1903 | "y": "24" 1904 | }, 1905 | "2019-02-20": { 1906 | "fill": "#ebedf0", 1907 | "count": 0, 1908 | "x": "-31", 1909 | "y": "36" 1910 | }, 1911 | "2019-02-21": { 1912 | "fill": "#c6e48b", 1913 | "count": 2, 1914 | "x": "-31", 1915 | "y": "48" 1916 | }, 1917 | "2019-02-22": { 1918 | "fill": "#ebedf0", 1919 | "count": 0, 1920 | "x": "-31", 1921 | "y": "60" 1922 | }, 1923 | "2019-02-23": { 1924 | "fill": "#7bc96f", 1925 | "count": 3, 1926 | "x": "-31", 1927 | "y": "72" 1928 | }, 1929 | "2019-02-24": { 1930 | "fill": "#7bc96f", 1931 | "count": 3, 1932 | "x": "-32", 1933 | "y": "0" 1934 | }, 1935 | "2019-02-25": { 1936 | "fill": "#ebedf0", 1937 | "count": 0, 1938 | "x": "-32", 1939 | "y": "12" 1940 | }, 1941 | "2019-02-26": { 1942 | "fill": "#ebedf0", 1943 | "count": 0, 1944 | "x": "-32", 1945 | "y": "24" 1946 | }, 1947 | "2019-02-27": { 1948 | "fill": "#c6e48b", 1949 | "count": 1, 1950 | "x": "-32", 1951 | "y": "36" 1952 | }, 1953 | "2019-02-28": { 1954 | "fill": "#c6e48b", 1955 | "count": 1, 1956 | "x": "-32", 1957 | "y": "48" 1958 | }, 1959 | "2019-03-01": { 1960 | "fill": "#ebedf0", 1961 | "count": 0, 1962 | "x": "-32", 1963 | "y": "60" 1964 | }, 1965 | "2019-03-02": { 1966 | "fill": "#c6e48b", 1967 | "count": 1, 1968 | "x": "-32", 1969 | "y": "72" 1970 | }, 1971 | "2019-03-03": { 1972 | "fill": "#c6e48b", 1973 | "count": 1, 1974 | "x": "-33", 1975 | "y": "0" 1976 | }, 1977 | "2019-03-04": { 1978 | "fill": "#ebedf0", 1979 | "count": 0, 1980 | "x": "-33", 1981 | "y": "12" 1982 | }, 1983 | "2019-03-05": { 1984 | "fill": "#ebedf0", 1985 | "count": 0, 1986 | "x": "-33", 1987 | "y": "24" 1988 | }, 1989 | "2019-03-06": { 1990 | "fill": "#ebedf0", 1991 | "count": 0, 1992 | "x": "-33", 1993 | "y": "36" 1994 | }, 1995 | "2019-03-07": { 1996 | "fill": "#7bc96f", 1997 | "count": 3, 1998 | "x": "-33", 1999 | "y": "48" 2000 | }, 2001 | "2019-03-08": { 2002 | "fill": "#c6e48b", 2003 | "count": 2, 2004 | "x": "-33", 2005 | "y": "60" 2006 | }, 2007 | "2019-03-09": { 2008 | "fill": "#c6e48b", 2009 | "count": 1, 2010 | "x": "-33", 2011 | "y": "72" 2012 | }, 2013 | "2019-03-10": { 2014 | "fill": "#c6e48b", 2015 | "count": 1, 2016 | "x": "-34", 2017 | "y": "0" 2018 | }, 2019 | "2019-03-11": { 2020 | "fill": "#ebedf0", 2021 | "count": 0, 2022 | "x": "-34", 2023 | "y": "12" 2024 | }, 2025 | "2019-03-12": { 2026 | "fill": "#c6e48b", 2027 | "count": 1, 2028 | "x": "-34", 2029 | "y": "24" 2030 | }, 2031 | "2019-03-13": { 2032 | "fill": "#c6e48b", 2033 | "count": 1, 2034 | "x": "-34", 2035 | "y": "36" 2036 | }, 2037 | "2019-03-14": { 2038 | "fill": "#c6e48b", 2039 | "count": 2, 2040 | "x": "-34", 2041 | "y": "48" 2042 | }, 2043 | "2019-03-15": { 2044 | "fill": "#c6e48b", 2045 | "count": 1, 2046 | "x": "-34", 2047 | "y": "60" 2048 | }, 2049 | "2019-03-16": { 2050 | "fill": "#ebedf0", 2051 | "count": 0, 2052 | "x": "-34", 2053 | "y": "72" 2054 | }, 2055 | "2019-03-17": { 2056 | "fill": "#ebedf0", 2057 | "count": 0, 2058 | "x": "-35", 2059 | "y": "0" 2060 | }, 2061 | "2019-03-18": { 2062 | "fill": "#c6e48b", 2063 | "count": 1, 2064 | "x": "-35", 2065 | "y": "12" 2066 | }, 2067 | "2019-03-19": { 2068 | "fill": "#ebedf0", 2069 | "count": 0, 2070 | "x": "-35", 2071 | "y": "24" 2072 | }, 2073 | "2019-03-20": { 2074 | "fill": "#c6e48b", 2075 | "count": 2, 2076 | "x": "-35", 2077 | "y": "36" 2078 | }, 2079 | "2019-03-21": { 2080 | "fill": "#c6e48b", 2081 | "count": 1, 2082 | "x": "-35", 2083 | "y": "48" 2084 | }, 2085 | "2019-03-22": { 2086 | "fill": "#196127", 2087 | "count": 11, 2088 | "x": "-35", 2089 | "y": "60" 2090 | }, 2091 | "2019-03-23": { 2092 | "fill": "#c6e48b", 2093 | "count": 2, 2094 | "x": "-35", 2095 | "y": "72" 2096 | }, 2097 | "2019-03-24": { 2098 | "fill": "#c6e48b", 2099 | "count": 2, 2100 | "x": "-36", 2101 | "y": "0" 2102 | }, 2103 | "2019-03-25": { 2104 | "fill": "#c6e48b", 2105 | "count": 2, 2106 | "x": "-36", 2107 | "y": "12" 2108 | }, 2109 | "2019-03-26": { 2110 | "fill": "#c6e48b", 2111 | "count": 1, 2112 | "x": "-36", 2113 | "y": "24" 2114 | }, 2115 | "2019-03-27": { 2116 | "fill": "#c6e48b", 2117 | "count": 2, 2118 | "x": "-36", 2119 | "y": "36" 2120 | }, 2121 | "2019-03-28": { 2122 | "fill": "#7bc96f", 2123 | "count": 4, 2124 | "x": "-36", 2125 | "y": "48" 2126 | }, 2127 | "2019-03-29": { 2128 | "fill": "#7bc96f", 2129 | "count": 5, 2130 | "x": "-36", 2131 | "y": "60" 2132 | }, 2133 | "2019-03-30": { 2134 | "fill": "#196127", 2135 | "count": 10, 2136 | "x": "-36", 2137 | "y": "72" 2138 | }, 2139 | "2019-03-31": { 2140 | "fill": "#7bc96f", 2141 | "count": 3, 2142 | "x": "-37", 2143 | "y": "0" 2144 | }, 2145 | "2019-04-01": { 2146 | "fill": "#ebedf0", 2147 | "count": 0, 2148 | "x": "-37", 2149 | "y": "12" 2150 | }, 2151 | "2019-04-02": { 2152 | "fill": "#c6e48b", 2153 | "count": 1, 2154 | "x": "-37", 2155 | "y": "24" 2156 | }, 2157 | "2019-04-03": { 2158 | "fill": "#ebedf0", 2159 | "count": 0, 2160 | "x": "-37", 2161 | "y": "36" 2162 | }, 2163 | "2019-04-04": { 2164 | "fill": "#c6e48b", 2165 | "count": 2, 2166 | "x": "-37", 2167 | "y": "48" 2168 | }, 2169 | "2019-04-05": { 2170 | "fill": "#c6e48b", 2171 | "count": 1, 2172 | "x": "-37", 2173 | "y": "60" 2174 | }, 2175 | "2019-04-06": { 2176 | "fill": "#ebedf0", 2177 | "count": 0, 2178 | "x": "-37", 2179 | "y": "72" 2180 | }, 2181 | "2019-04-07": { 2182 | "fill": "#ebedf0", 2183 | "count": 0, 2184 | "x": "-38", 2185 | "y": "0" 2186 | }, 2187 | "2019-04-08": { 2188 | "fill": "#c6e48b", 2189 | "count": 1, 2190 | "x": "-38", 2191 | "y": "12" 2192 | }, 2193 | "2019-04-09": { 2194 | "fill": "#c6e48b", 2195 | "count": 1, 2196 | "x": "-38", 2197 | "y": "24" 2198 | }, 2199 | "2019-04-10": { 2200 | "fill": "#c6e48b", 2201 | "count": 2, 2202 | "x": "-38", 2203 | "y": "36" 2204 | }, 2205 | "2019-04-11": { 2206 | "fill": "#c6e48b", 2207 | "count": 1, 2208 | "x": "-38", 2209 | "y": "48" 2210 | }, 2211 | "2019-04-12": { 2212 | "fill": "#7bc96f", 2213 | "count": 5, 2214 | "x": "-38", 2215 | "y": "60" 2216 | }, 2217 | "2019-04-13": { 2218 | "fill": "#ebedf0", 2219 | "count": 0, 2220 | "x": "-38", 2221 | "y": "72" 2222 | }, 2223 | "2019-04-14": { 2224 | "fill": "#c6e48b", 2225 | "count": 1, 2226 | "x": "-39", 2227 | "y": "0" 2228 | }, 2229 | "2019-04-15": { 2230 | "fill": "#7bc96f", 2231 | "count": 4, 2232 | "x": "-39", 2233 | "y": "12" 2234 | } 2235 | }, 2236 | "orgs": { 2237 | "bowlingjs": "https://avatars3.githubusercontent.com/u/8825909?s=70&v=4", 2238 | "foundersandcoders": "https://avatars3.githubusercontent.com/u/9970257?s=70&v=4", 2239 | "docdis": "https://avatars0.githubusercontent.com/u/10836426?s=70&v=4", 2240 | "dwyl": "https://avatars2.githubusercontent.com/u/11708465?s=70&v=4", 2241 | "ladiesofcode": "https://avatars0.githubusercontent.com/u/16606192?s=70&v=4", 2242 | "TheScienceMuseum": "https://avatars0.githubusercontent.com/u/16609662?s=70&v=4", 2243 | "SafeLives": "https://avatars2.githubusercontent.com/u/20841400?s=70&v=4" 2244 | } 2245 | } -------------------------------------------------------------------------------- /test/fixtures/repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "/dwyl/health", 3 | "type": "repo", 4 | "description": "🍏 🍋 🍓 🍐 🍌 🍍 🍉 🍒", 5 | "website": "", 6 | "tags": "read, the, issues", 7 | "watchers": 3, 8 | "stars": 6, 9 | "forks": 0, 10 | "commits": 1, 11 | "branches": 1, 12 | "releases": 0, 13 | "langs": [] 14 | } -------------------------------------------------------------------------------- /test/fixtures/stargazers.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=88&v=4", 5 | "uid": 6057298, 6 | "username": "SimonLab" 7 | }, 8 | { 9 | "avatar": "https://avatars1.githubusercontent.com/u/11595920?s=88&v=4", 10 | "uid": 11595920, 11 | "username": "rub1e" 12 | }, 13 | { 14 | "avatar": "https://avatars1.githubusercontent.com/u/12380455?s=88&v=4", 15 | "uid": 12380455, 16 | "username": "bradreeder" 17 | }, 18 | { 19 | "avatar": "https://avatars3.githubusercontent.com/u/4200487?s=88&v=4", 20 | "uid": 4200487, 21 | "username": "JoseCage" 22 | }, 23 | { 24 | "avatar": "https://avatars1.githubusercontent.com/u/4185328?s=88&v=4", 25 | "uid": 4185328, 26 | "username": "iteles" 27 | }, 28 | { 29 | "avatar": "https://avatars3.githubusercontent.com/u/5038030?s=88&v=4", 30 | "uid": 5038030, 31 | "username": "tunnckoCore" 32 | } 33 | ], 34 | "url": "/dwyl/health/stargazers", 35 | "type": "stars" 36 | } -------------------------------------------------------------------------------- /test/server.test.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test; 2 | const server = require('../server/server.js'); 3 | const request = require('supertest')(server.url); 4 | 5 | test('connect to server', function (t) { 6 | request.get('/') 7 | .expect(200) 8 | .end(function(err, res) { 9 | if (err) throw err; 10 | server.close(function () { console.log('Server closed!'); }); 11 | t.end() 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); // see: github.com/dwyl/learn-tape 2 | const utils = require('../server/utils'); 3 | 4 | tap.test('utils.log_error', function testfn (t) { 5 | const error = 'DON\'T PANIC! This is only a utils.log_error test execution ☔️' 6 | utils.log_error(error, { "hello": "world"}, new Error().stack); 7 | utils.log_error(error, { "hello": "world"}); 8 | t.end(); 9 | }); 10 | 11 | tap.test('utils.exec_cb', function(t) { 12 | const error = 'DON\'T PANIC! This is only a utils.exec_cb test ☔️ ' 13 | // call without params: 14 | utils.exec_cb(); // no expectation but also no error! 15 | utils.exec_cb(function callback (e, data) { 16 | t.equal(e, error, 'woohoo our exec_cb works as expected!'); 17 | t.equal(data, 'hai', 'exec_cb simply executes the callback'); 18 | t.end(); 19 | }, error, 'hai'); 20 | }); 21 | 22 | tap.test('utils.recent_activity', function(t) { 23 | const person = require('./fixtures/person.json'); 24 | const recent_activity = utils.recent_activity(person); 25 | t.ok(recent_activity > 0, 'recent_activity: ' + recent_activity) 26 | t.end(); 27 | }); 28 | -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | # Why? 2 | 3 | This is a self-contained tutorial for people learning 4 | PostgreSQL for the _first_ time. 5 | 6 | 7 | 8 | ## People 9 | 10 | 11 | psql -U postgres -d codeface -a -f schema.sql 12 | --------------------------------------------------------------------------------