Full Code of dwyl/learn-postgresql for AI

main 5f68be16807e cached
28 files
101.7 KB
34.2k tokens
26 symbols
1 requests
Download .txt
Repository: dwyl/learn-postgresql
Branch: main
Commit: 5f68be16807e
Files: 28
Total size: 101.7 KB

Directory structure:
gitextract__dzctz4a/

├── .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

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directory
node_modules

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history
package-lock.json
.nyc_output/


================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - "node"
env:
  global:
    - DATABASE_URL=postgres://postgres:@localhost/codeface
    - NODE_ENV=TEST
services:
  - postgresql
after_success:
  - bash <(curl -s https://codecov.io/bash)


================================================
FILE: FAQ.md
================================================

## Why PostgreSQL and _Not_ MySQL?

The _good_ news is that almost all of your PostgreSQL knowledge
is _directly_ transferable to [MySQL](https://en.wikipedia.org/wiki/MySQL).
Since _both_ use SQL as the language
for interacting with the database,
the time you invest in learning PostgreSQL
and building SQL skills is a hugely valuable.

Learning how to _run_ means you also know how to _walk_.
PostgreSQL might _feel_ "more difficult"
in the same way that , but the principals are all the same.
Just stick with it and keep asking questions until it all "makes sense".
If you need to _apply_ your SQL skills to MySQL, MS SQL or MariaDB,
it will only take you a few minutes to adapt to it.

It's very much like riding a bicycle.
Once you know how to balance, pedal and steer,
your skills transfer to other bicycles.



The _reason_ MySQL is still _hugely_ popular
can be summarised by _one_ word:
[***WordPress***](https://en.wikipedia.org/wiki/WordPress).

Over **30%** of the 10 million most popular websites use WordPress.
WordPress runs on the "LAMP" (_Linux Apache **MySQL** PHP_) stack,
which means that people are using MySQL by _`default`_
not _conscious enlightenment_.
+ https://www.whoishostingthis.com/compare/wordpress/stats
+ https://w3techs.com/technologies/overview/content_management/all


## Why _Not_ Use WordPress?

WordPress is _unquestionably_ a good CMS and blogging platform
that helps millions of people/businesses publish online.
Sadly, it's not secure by _default_ and when a vulnerability is discovered,
it gets exploited en-mass very quickly.
Yes, WordPress can be
["Hardened"](https://codex.wordpress.org/Hardening_WordPress)
but that is _usually_ not the _first_ thing on people's todo list
when launching a website or blog.
The result is that _thousands_ of WordPress websites get hacked
each time a patch is released e.g:
https://www.zdnet.com/article/thousands-of-wordpress-sites-backdoored-with-malicious-code
and it creates a maintenance headache
for the person/people _responsible_ for the site.
We're not saying you (_or anyone else_) should not use WordPress,
just make sure you follow the the latest "best practice" if you do.
(_We have been "burned" by it through no fault of our own...
and would not touch it again with a barge pole!
There are **much** more **secure** and **performant** options!_)

### What About NoSQL Databases/Datastores Like ElasticSearch and Redis?

@dwyl we are _huge_ fans of _special-purpose_ data storage/retrieval systems.
We have used _several_ NoSQL databases including CouchDB, ElasticSearch,
MongoDB, Neo4J and Redis.
Of these we _recommend_ ElasticSearch for full-text search
and Redis for in-momory datasets and caching. see:

+ [github.com/dwyl/learn-**elasticsearch**](https://github.com/dwyl/learn-elasticsearch)
+ [github.com/dwyl/learn-**redis**](https://github.com/dwyl/learn-redis)

However as a "primary" datastore with a robust query language,
we feel PostgreSQL is the _clear_ winner as a "first" database.


================================================
FILE: README.md
================================================
<div align="center">

# Learn PostgreSQL

Learn how to use PostgreSQL
and Structured Query Language (SQL) to store
and query your data.

<br />

<a href="https://www.postgresql.org/about/">
  <img src="https://user-images.githubusercontent.com/194400/52590350-fa116100-2e38-11e9-9303-c38819493a4e.png" width="700">
</a>

<br />

[![Build Status](https://img.shields.io/travis/dwyl/learn-postgresql/master.svg?style=flat-square)](https://travis-ci.org/dwyl/learn-postgresql)
[![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)
[![Dependencies: None!](https://david-dm.org/dwyl/learn-postgresql/status.svg?style=flat-square)](https://david-dm.org/dwyl/learn-postgresql)
[![devDependencies Status](https://david-dm.org/dwyl/learn-postgresql/dev-status.svg?style=flat-square)](https://david-dm.org/dwyl/learn-postgresql?type=dev)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/learn-postgresql/issues)
<!-- uncomment when ready
[![HitCount](https://hits.dwyl.io/dwyl/learn-postgresql.svg)](https://hits.dwyl.io/dwyl/learn-postgresql)
-->

</div>


# _Why_?

Helping people store, retrieve and derive insights from data
is the essence of _all_ software applications. <br />

## SQL is _Everywhere_

Like it or not, Relational Databases store
_most_ of the world's structured data
and Structured Query Language (SQL)
is _by far_ the most frequent way of retrieving the data.<br />

According to the most _recent_ surveys/statistics,
SQL _still_ dominates the world of databases.

https://insights.stackoverflow.com/survey/2018/#technology-databases
![stackoverflow-survey-2018-databases](https://user-images.githubusercontent.com/194400/52594468-80cb3b80-2e43-11e9-867a-eeb4eea9a322.png)


https://db-engines.com/en/ranking
![dbms-ranking](https://user-images.githubusercontent.com/194400/52594416-64c79a00-2e43-11e9-8a61-02af22554802.png)

> _**Note**: you should never adopt a technology
based on it's **current popularity**,
also be ware of_
["_argumentum ad populum_"](https://en.wikipedia.org/wiki/Argumentum_ad_populum)
("_it's popular therefore you should use it_").
_Always pick the **appropriate tool** for the job
based on the requirements, constraints and/or availability
(both of "skill" on your existing team or in the wider community).
We include these stats to explain that **relational databases**
are **still** the most widely used **by far** and so
learning SQL skills is a very **wise investment**
both as an **individual** and for your **team** or **organisation**._


## PostgreSQL is _Easy_ to Learn and it Runs _Everywhere_!

Getting started with PostgreSQL is _easy_,
(_just follow the steps in this guide and try out the example queries!_) <br />
When you are ready to _deploy_ your app, you are in safe hands,
PostgreSQL runs _everywhere_:

+ **Travis-CI** (free) Integration Testing:
https://docs.travis-ci.com/user/database-setup/#postgresql
+ **Heroku** PostgreSQL (_free for MVP: 10k rows_): https://www.heroku.com/postgres
+ AWS RDS Postgres (_good value + high performance_):
https://aws.amazon.com/rds/postgresql/
+ Google Cloud SQL: https://cloud.google.com/sql/
+ DigitalOcean: https://www.digitalocean.com/products/managed-databases/
+ Linode:
https://www.linode.com/docs/databases/postgresql/create-a-highly-available-postgresql-cluster-using-patroni-and-haproxy/
+ Azure: https://azure.microsoft.com/en-us/services/postgresql/
  + Citus: https://techcrunch.com/2019/01/24/microsoft-acquires-citus-data
+ Self-managed high availability cluster: https://github.com/sorintlab/stolon

# _Who_?

_Everyone_ building _any_ application that stores data should learn SQL.
SQL is _ubiquitous_ in every field/industry and the sooner you learn/master it,
the higher your life-time return on time investment.

Learning how to use a relational database is a foundational skill
for all of computer science and application development.

Being _proficient_ in SQL will open the door to Data Science with
[SQL-on-Hadoop](https://mapr.com/why-hadoop/sql-hadoop/sql-hadoop-details/)
[Apache Spark](https://en.wikipedia.org/wiki/Apache_Spark#Spark_SQL),
Google [BigQuery](https://en.wikipedia.org/wiki/BigQuery),
[Oracle](https://en.wikipedia.org/wiki/Oracle_Corporation#Controversies)
and [Teradata](https://en.wikipedia.org/wiki/Teradata).
In short, get _really_ good at SQL! It's _very_ useful.

# _What_?

This tutorial covers 5 areas:

1. _What_ is PostgreSQL?
2. _How_ do I get _started_ with PostgreSQL? (_a fully functioning example!_)
3. What is Structured Query Language (SQL)? (_lots of example queries!_)
4. _How_ do I write my _own_ SQL Queries?
5. _How_ do I deploy my own PostgreSQL-based Application?

Once you have covered these areas,
you will _know_ if PostgreSQL
is "right" for your needs,
or if you need to keep looking for a different way
to store data.

Let's dive in!

## 1. What is PostgreSQL?

PostgreSQL (_often shortened to simply "**Postgres**"_)
is an advanced
**R**elational **D**ataBase **M**anagement **S**ystem ("RDBMS"),
that lets you _efficiently_ and _securely_ store _any_ type of data.
We will _explain_ "Relational Database" in the context
of our _example_ below,
so don't worry if it sounds like a buzzword soup.

Postgres has an emphasis on standards compliance and extensibility
which means there are many plugins you can use to enhance it
like [PostGIS](https://postgis.net) for mapping applications
and entire projects built on top of it like
[TimescaleDB](https://www.timescale.com/)
(_a time-series database perfect for analytics_)
and [AgensGraph](https://bitnine.net/)
(_a graph database, great for modelling networks e.g a "social graph"_).


Structured Query Language (SQL)
is the preferred means of interacting with data at any scale. <br />

> The _only_ reason MySQL is still more widely used than Postgres
can be summarised in *one word*: **WordPress**.
> WordPress has a firm grip on the CMS-based website market
and it shows no sign of slowing down.
> If your goal is to build CMS-based websites,
or the company you _already_ work for uses
[WordPress](https://www.cvedetails.com/vendor/2337/Wordpress.html),
you should go for it!
> If you prefer a more _general_ introduction to SQL,
> follow _this_ tutorial!
> The knowledge you will gain by learning Postgres is 95%+
"_transferable_" to other SQL databases so don't worry about
the differences between MySQL and Postgres for now.
If you're curious, read: https://hackr.io/blog/postgresql-vs-mysql


<!-- I'm undecided on the following section ...
I don't want to be seen to be "bashing" MongoDB ...

Unless you work for [MongoDB](https://en.wikipedia.org/wiki/MongoDB#Security)
(_or another [NoSQL](https://en.wikipedia.org/wiki/NoSQL) database company_)
the chance that you will encounter a Relational Database
and thus benefit from knowing Structured Query Language (SQL)
in your _career_ as a software engineer tends toward 100%.

If you are _tempted_ to use MongoDB as your _primary_ data store,
read this:
https://www.theguardian.com/info/2018/nov/30/bye-bye-mongo-hello-postgres
MongoDB is _meant_ to be a "document-oriented database",
so it should be _perfect_ for a CMS (_document content_).
And yet, after years of _trying_ to make it work,
the _highly_ competent engineers at The Guardian
decided it wasn't worth the hassle and switched to PostgreSQL to _great_ result!
Read the Hacker News thread: https://news.ycombinator.com/item?id=18717168

If you are _tempted_ by MongoDB because of the "***MEAN***" **stack**,
by all means dive into trying it. We have been there and seen the appeal.
e.g: https://github.com/dwyl/mongo-search ...
(_We learned the "hard way"
that Searching MongoDB was slow and inconsistent,
PostgreSQL has **much better** full-text search
or use ElasticSearch!_)
However we _urge_ you to understand
that the _benefit_ of getting _started_ fast
(_because your MongoDB records don't have a pre-defined schema_)
will _quickly_ wear off when queries become convoluted and _slow_
(_because there are no **real** way to do multiple "JOINs"
and index optimisation is laborious_).

Another important factor
(_for us @dwyl because we are
  [security](https://github.com/dwyl/learn-security/)-conscious_)
is the fact that MongoDB is _insecure_ by **`default`** see:
https://en.wikipedia.org/wiki/MongoDB#Security ...
We cannot stand the idea of insecurely storing _anyone's_ data,
and PostgreSQL makes it _easy_ to add advanced security, access controls
table-level permissions and field-level data encryption.

-->

# _How_?

### Installation

Before you get started with using PostgreSQL, you'll have to install it.
Follow these steps to get started:

#### MacOS

1. There are a couple of ways to install PostgreSQL. One of the easier ways to
get started is with Postgres.app. Navigate to https://postgresapp.com/ and then
click "Download":
![download](https://cloud.githubusercontent.com/assets/12450298/19641848/6d3cfa4a-99da-11e6-858f-3ff2ada026be.png)

2. Once it's finished downloading, double click on the file to unzip then move
the PostgreSQL elephant icon into your `applications` folder. Double click the
icon to launch the application.

3. You should now see a new window launched with a list of servers to the left side of the window 
(if it's a fresh install, you should see one named `PostgreSQL XX`). 
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. 
To fully reinstall follow [these steps](https://postgresapp.com/documentation/install.html) to delete data directories and preferences. 
Click on the button 'Initialize' (or 'Start' if you had already installed previously).
<img width="718" alt="download" src="https://user-images.githubusercontent.com/17494745/195095742-e6838922-b17a-495d-b922-71f1ddfcd581.png">

4. Run `sudo mkdir -p /etc/paths.d && echo /Applications/Postgres.app/Contents/Versions/latest/bin | sudo tee /etc/paths.d/postgresapp` 
(found [here](https://postgresapp.com/documentation/install.html)) to use `psql` in the terminal. 
Close and open the terminal.

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`.

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'):

![terminal](https://cloud.githubusercontent.com/assets/12450298/19642816/f8ac0c66-99de-11e6-87e2-db55e6abc27b.png)

7. You should now be all set up to start using PostgreSQL. For documentation on
command line tools etc see https://postgresapp.com/documentation/

#### Ubuntu

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.

##### Installation

```
sudo apt-get update
sudo apt-get install postgresql postgresql-contrib
```

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:

```
sudo -u postgres createuser --interactive
```

This command means 'run the command `createuser --interactive` as the user called "postgres"'.

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`.

When asked if you want to make the role a superuser, type 'y'.

We now need to create the database matching the role name, as PostgreSQL expects this. Run:

```
sudo -u postgres createdb [your user name]
```

You can now connect to PostgreSQL by running `psql`.

### Create your first PostgreSQL database

1. To start PostgreSQL, type this command into the terminal:  
`psql`  

2. Next type this command into the PostgreSQL interface:  
`CREATE DATABASE test;`  
**NOTE:** Don't forget the semi-colon. If you do, useful error messages won't
show up.

3. To check that our database has been created, type `\l` into the psql prompt.
You should see something like this in your terminal:
![test db](https://cloud.githubusercontent.com/assets/12450298/19650613/ce278678-9a01-11e6-89ad-b124c0adcfe5.png)

### Create new users for your database

1. If you closed the PostgreSQL server, start it again with:  
` psql`  

2. To create a new user, type the following into the psql prompt:  
    ```sql
    CREATE USER testuser;
    ```

3. Check that your user has been created. Type `\du` into the prompt. You should
see something like this:
![user](https://cloud.githubusercontent.com/assets/12450298/19650852/9c340708-9a02-11e6-8f06-75f1e30a86b3.png)
Users can be given certain permissions to access any given database you have
created.

4. Next we need to give our user permissions to access the test database we
created above. Enter the following command into the `psql` prompt:  
    ```sql
    GRANT ALL PRIVILEGES ON DATABASE test TO testuser;
    ```


### PostGIS - Spacial and Geographic objects for PostgreSQL

#### PostGIS Installation
If you've installed Postgres App as in the example above, you can easily
extend it to include PostGIS. Follow these steps to begin using PostGIS:

1. Ensure that you're logged in as a user OTHER THAN `postgres`. Follow the
steps above to enable your default user to be able to access the `psql` prompt.
(_[installation step 7](#installation)_)

2. Type the following into the `psql` prompt to add the extension:  
`CREATE EXTENSION postgis;`

#### PostGIS Distance between two sets of coordinates

After you've extended PostgreSQL with PostGIS you can begin to use it. Type
the following command into the `psql` command line:  

```sql
SELECT ST_Distance(gg1, gg2) As spheroid_dist
FROM (SELECT
	ST_GeogFromText('SRID=4326;POINT(-72.1235 42.3521)') As gg1,
	ST_GeogFromText('SRID=4326;POINT(-72.1235 43.1111)') As gg2
	) As foo  ;
```

This should return `spheroid_dist` along with a value in meters. The
example above returns: `84315.42034614` which is rougly 84.3km between the two
points.

### Commands
Once you are serving the database from your computer

- To change db
`\connect database_name;`

- To see the tables in the database
`\d;`

- To select (and show in terminal) all tables
`SELECT * FROM table_name`


- To make a table
`CREATE TABLE table_name (col_name1, col_name2)`

- To add a row
`INSERT INTO table_name ( col_name )
VALUES ( col_value)`
col_name only require if only some of the cols are being filled out

- To edit a column to a table 
`ALTER TABLE table_name
  ALTER COLUMN column_name SET DEFAULT expression`

- To add a column to a table 
`ALTER TABLE table_name
  ADD COLUMN column_name data_type`

- To find the number of instances where the word “Day” is present in the title of a table
`SELECT count(title) FROM table_name WHERE title LIKE '%Day%’;`

- To delete a row in a table
`DELETE FROM table_name
  WHERE column_name = ‘hello';`


Postgresql follows the SQL convention of calling relations TABLES, attributes COLUMNs and tuples ROWS

**Transaction**
All or nothing, if something fails the other commands are rolled back like nothing happened

**Reference**
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.

```sql
CREATE TABLE cities (
  name text NOT NULL,
  postal_code varchar(9) CHECK (postal_code <> ''),
  country_code char(2) REFERENCES countries,
  PRIMARY KEY (country_code, postal_code)
);
```

`<>` means not equal


**Join reads**
You can join tables together when reading them,

**Inner Join**
Joins together two tables by specifying a column in each to join them by i.e.

```sql
SELECT cities.*, country_name
  FROM cities INNER JOIN countries
  ON cities.country_code = countries.country_code;
```

This will select all of the columns in both the countries
and cities tables the data, the rows are matched up by `country_code`.

**Grouping**
You can put rows into groups where the group is defined by a shared value in a particular column.

```sql
SELECT venue_id, count(*)
  FROM events
  GROUP BY venue_id;
```

This will group the rows together by the venue_id,
count is then performed on each of the groups.

### Learning Resources

+ Node-hero: https://blog.risingstack.com/node-js-database-tutorial
+ Pluralsight postgres getting started:
  https://www.pluralsight.com/courses/postgresql-getting-started
+ Tech Republic Postgres setup:
  https://www.techrepublic.com/article/diy-a-postgresql-database-server-setup-anyone-can-handle/
+ PostGIS installation: https://postgis.net/install
+ PostGIS docs: https://postgis.net/docs/manual-2.3
+ SQl Tutorials: https://www.scaler.com/topics/sql/
+ PostGIS ST_Distance: https://postgis.net/docs/ST_Distance.html
+ Foreign Key Constraints:
  + https://en.wikipedia.org/wiki/Foreign_key
  + https://www.postgresqltutorial.com/postgresql-tutorial/postgresql-foreign-key/
  + https://tableplus.io/blog/2018/08/postgresql-how-to-add-a-foreign-key.html
+ Graphical Interface (GUI) tools:
https://wiki.postgresql.org/wiki/Community_Guide_to_PostgreSQL_GUI_Tools


================================================
FILE: client/index.html
================================================
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Time MVP (Title Gets Over-ridden with Clock ;-)</title>
    <link rel="shortcut icon" type="image/png"
    href="https://cloud.githubusercontent.com/assets/194400/25605640/15c23162-2f04-11e7-8371-228cf5bf61e2.png"/>
    <link rel="stylesheet" href="https://unpkg.com/tachyons@4.6.1/css/tachyons.min.css"/>
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
  </head>

  <body>
    <main id="main">
      <h1>Hello World 123!</h1>
    </main>

  <script src='https://rawgit.com/dwyl/faster/master/lib/client.js'></script>
  </body>
</html>


================================================
FILE: install.md
================================================
# DBeaver

See: https://github.com/dwyl/learn-postgresql/issues/43#issuecomment-469000357


================================================
FILE: package.json
================================================
{
  "name": "learn-postgresql",
  "version": "1.0.0",
  "description": "PostgreSQL Tutorial",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "drop": "psql -U postgres -c 'DROP DATABASE IF EXISTS codeface;'",
    "create": "psql -U postgres -c 'CREATE DATABASE codeface;'",
    "schema": "psql -U postgres -d codeface -a -f schema.sql",
    "recreate": "npm run drop && npm run create && npm run schema",
    "test": "nyc tap ./test/*.test.js | tap-nyc",
    "quick": "tap ./test/*.test.js",
    "start": "node server/server.js",
    "faster": "./node_modules/faster/bin/faster.js",
    "postinstall": "npm run recreate",
    "open-cov": "open ./coverage/lcov-report/index.html"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/dwyl/learn-postgresql.git"
  },
  "keywords": [
    "PostgreSQL",
    "Postgres",
    "Beginners",
    "Tutorial"
  ],
  "author": "dwyl & friends",
  "license": "GPL-2.0",
  "bugs": {
    "url": "https://github.com/dwyl/learn-postgresql/issues"
  },
  "homepage": "https://github.com/dwyl/learn-postgresql#readme",
  "dependencies": {
    "github-scraper": "^6.7.0",
    "pg": "^7.8.1"
  },
  "devDependencies": {
    "faster": "^3.5.1",
    "nyc": "^13.1.0",
    "supertest": "^4.0.2",
    "tap": "^12.6.1",
    "tap-nyc": "^1.0.3"
  },
  "nyc": {
    "check-coverage": true,
    "lines": 100,
    "statements": 100,
    "functions": 100,
    "branches": 100,
    "include": [
      "server/*.js"
    ],
    "exclude": [
      "test/*.test.js"
    ],
    "reporter": [
      "lcov",
      "text-summary"
    ],
    "cacheDirectories": [
      "node_modules"
    ],
    "all": true,
    "report-dir": "./coverage"
  }
}


================================================
FILE: query.sql
================================================
SELECT
 next_page,
 COUNT (next_page) AS c
FROM
 logs
WHERE next_page IS NOT null
AND next_page NOT IN (
    SELECT path
    FROM logs
    WHERE path IS NOT NULL
)
GROUP BY
 next_page
ORDER BY
 c ASC
LIMIT 1;


================================================
FILE: schema.sql
================================================
CREATE TABLE IF NOT EXISTS "people" (
  "inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	"id" SERIAL PRIMARY KEY,
	"name" VARCHAR(50) DEFAULT NULL,
	"username" VARCHAR(50) NOT NULL,
	"bio" VARCHAR(255) DEFAULT NULL,
	"worksfor" VARCHAR(50) DEFAULT NULL,
	"uid" INT NOT NULL, -- the person's GitHub uid e.g: 4185328
	"location" VARCHAR(100) DEFAULT NULL,
	"website" VARCHAR(255) DEFAULT NULL,
	"stars" INT DEFAULT 0,
	"followers" INT DEFAULT 0,
	"following" INT DEFAULT 0,
	"contribs" INT DEFAULT 0,
	"recent_activity" INT DEFAULT 0
);

CREATE TABLE IF NOT EXISTS "orgs" (
	"inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	"id" SERIAL PRIMARY KEY,
	"name" VARCHAR(50) DEFAULT NULL,
	"url" VARCHAR(50),
	"description" VARCHAR(255) DEFAULT NULL,
	"location" VARCHAR(50) DEFAULT NULL,
	"website" VARCHAR(255) DEFAULT NULL,
	"email" VARCHAR(255) DEFAULT NULL,
	"pcount" INT DEFAULT 0,
	"uid" INT NOT NULL
);

CREATE TABLE IF NOT EXISTS "repos" (
	"inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	"id" SERIAL PRIMARY KEY,
	"url" VARCHAR(255) NOT NULL, -- know what the char limit is for a repo name?
	"description" TEXT DEFAULT NULL,
	"website" VARCHAR(255) DEFAULT NULL,
	"watchers" INT DEFAULT 0,
	"stars" INT DEFAULT 0,
	"forks" INT DEFAULT 0,
	"commits" INT DEFAULT 0,
	"langs" VARCHAR(255) DEFAULT NULL,
	"tags" TEXT DEFAULT NULL,
	"person_id" INT REFERENCES people (id), -- can be NULL if repo belongs to org.
	"org_id" INT REFERENCES orgs (id) -- this can be NULL if repo is personal.
);

CREATE TABLE IF NOT EXISTS "logs" (
	"id" SERIAL PRIMARY KEY,
	"inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	"url" VARCHAR(255) NOT NULL,
	"next_page" VARCHAR(255) DEFAULT NULL
);

CREATE TABLE IF NOT EXISTS "relationships" (
	"inserted_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	"id" SERIAL PRIMARY KEY,
	"person_id" INT REFERENCES people (id) DEFAULT NULL,
	"leader_id" INT REFERENCES people (id) DEFAULT NULL,
	"org_id" INT REFERENCES orgs (id) DEFAULT NULL,
	"repo_id" INT REFERENCES repos (id) DEFAULT NULL
);


================================================
FILE: server/bot.js
================================================
const db = require('./db');
const utils = require('./utils');
const gs = require('github-scraper');

function fetch (path, callback) {
  gs(path, function(error, data) {
    if (error) { // don't bother trying to save data if an error occurred
      utils.log_error(error, data, new Error().stack); // get exact stack trace.
      return utils.exec_cb(callback, error, data);
    }
    console.log('data.type:', data.type);
    switch (data.type) {
      case 'org':
        db.insert_org(data, callback);
        break;
      case 'profile':
        db.insert_person(data, callback);
        break;
      case 'repo':
        db.insert_repo(data, callback);
        break;
      case 'followers': // multiple cases same outcome.
      case 'following':
      case 'people':
      case 'stars':
        fetch_list_of_profiles_slowly(data, callback);
        break;
    }
  });
}

/**
 * fetch_list_of_profiles_slowly does what it's name suggests.
 * attempting to fetch GitHub profiles too quickly results in errors.
 * @param {object} data - should contain url and entries (a list of people).
 * @param {function} next - the function executed once profiles are saved.
 * @param {function} callback - the callback function to be executed if any.
 */
function fetch_list_of_profiles_slowly (data, callback) {
  const len = data.entries.length;

  data.entries.forEach((u, i) => { // poor person's "async parallel":

    setTimeout(function delayed_request () { // delay requests to avoid errors

      gs(u.username, function process (error, profile) {
        utils.log_error(error, profile, new Error().stack);

        db.insert_person(profile, function (err2, data2) {

          if (i == len - 1) { // only insert relationships once people records
            return db.insert_relationships(data, callback); // once per batch.
          }  // e.g: db.insert_stars(data, callback) in the case of 'stars' page
        });
      });
    }, i * 1000); // timer gets longer as i increases to avoid flooding!
  });
}

module.exports = {
  fetch: fetch
}


================================================
FILE: server/db.js
================================================
/* istanbul ignore next */
process.env.DATABASE_URL = process.env.DATABASE_URL
  || "postgres://postgres:@localhost/codeface";
const pg = require('pg');
const PG_CLIENT = new pg.Client(process.env.DATABASE_URL);
const utils = require('./utils');

console.log('db.js:L7: PG_CLIENT._connecting:', PG_CLIENT._connecting, // debug
  '| PG_CLIENT._connected:', PG_CLIENT._connected);

// auto-start pg connection when module is required so startup is faster!
connect(function (err, data) {
  console.log('db.js:L12: PG_CLIENT._connected:', PG_CLIENT._connected);
  console.log('- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -');
})

/**
 * connnect ensures that a postgres connection is available before continuing
 * @param {function} callback - function called once connection is confirmed.
 */
function connect (callback) {
  // console.log('L45: PG_CLIENT._connecting:', PG_CLIENT._connecting,
  //   '| PG_CLIENT._connected:', PG_CLIENT._connected);
  if (PG_CLIENT && !PG_CLIENT._connected && !PG_CLIENT._connecting) {
    PG_CLIENT.connect(function (error, data) {
      utils.log_error(error, data, new Error().stack);
      return utils.exec_cb(callback, error, PG_CLIENT);
    });
  } else {
    return utils.exec_cb(callback, null, PG_CLIENT);
  }
}

/**
 * end used in testing to end/close the Postgres connection:
 * @param {function} callback - callback function to be executed on success.
 */
function end (callback) {
  /* istanbul ignore else */
  if(PG_CLIENT && PG_CLIENT._connected && !PG_CLIENT._connecting) {
    PG_CLIENT.end(() => {
      return utils.exec_cb (callback, null, PG_CLIENT);
    });
  }
}

/**
 * insert_person saves a person's data to the people table.
 * @param {object} data - a valid JSON object containing data to be inserted.
 * @param {function} callback - callback function to be executed on success.
 */
function insert_person (data, callback) {
  connect( function insert_person_after_connected () {
    const { name, username, bio, worksfor, location, website, uid,
      stars, followers, following, contribs } = data;
    const recent_activity = utils.recent_activity(data);
    const query = `INSERT INTO people (name, username, bio, worksfor, location,
      website, uid, stars, followers, following, contribs, recent_activity)
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`;
    const values = [name, username, bio, worksfor, location, website, uid,
      stars, followers, following, contribs, recent_activity];

    PG_CLIENT.query(query, values, function(error, result) {
      utils.log_error(error, data, new Error().stack);
      return insert_next_page (data, callback);
    });
  });
}

/**
 * select_person gets the person from people table.
 * @param {string} username - username of the person e.g: 'iteles'
 * @param {function} callback - callback function to be executed on success.
 */
function select_person (username, callback) {
  connect( function select_person_after_connected () {
    const query = `SELECT * FROM people WHERE username = $1
      ORDER BY id ASC LIMIT 1`;
    PG_CLIENT.query(query, [username], function(error, result) {
      utils.log_error(error, result, new Error().stack);
      return utils.exec_cb(callback, error, result);
    });
  });
}

/**
 * insert_org saves an org's data to the orgs table.
 *
 */
function insert_org (data, callback) {
  connect( function insert_org_after_connected () {
    const { url,name,description,location,website,email,pcount,uid } = data;
    const query = `INSERT INTO orgs
    (url, name, description, location, website, email, pcount, uid)
    VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`;
    const values = [url,name,description,location,website,email,pcount,uid];

    PG_CLIENT.query(query, values, function(error, result) {
      utils.log_error(error, data, new Error().stack);
      return insert_next_page (data, callback);
    });
  });
}

/**
 * select_org retrieves the org for a given url.
 * @param {string} url - url of the repo (e.g: /dwyl)
 * @param {function} callback - callback function to be executed on success.
 */
function select_org (url, callback) {
  connect( function select_repo_after_connected () {
    const query = `SELECT * FROM orgs WHERE url = $1 ORDER BY id ASC LIMIT 1`;
    console.log(query, url);
    PG_CLIENT.query(query, [url], function(error, result) {
      utils.log_error(error, result, new Error().stack);
      return utils.exec_cb(callback, error, result);
    });
  });
}

/**
 * insert_repo saves an repo's stats to the repos table.
 * @param {object} data - a valid JSON object containing data to be inserted.
 * @param {function} callback - callback function to be executed on success.
 */
function insert_repo (data, callback) {
  connect( function insert_repo_after_connected () {
    const { url, description, website, tags, langs,
      watchers, stars, forks, commits} = data;
    const query = `INSERT INTO repos
    (url, description, website, tags, langs, watchers, stars, forks, commits)
    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`;
    const values = [url, description, website, tags, langs.join(','),
      watchers, stars, forks, commits];

    PG_CLIENT.query(query, values, function(error, result) {
      utils.log_error(error, data, new Error().stack);
      return insert_next_page (data, callback);
    });
  });
}

/**
 * select_repo retrieves the repo for a given url.
 * @param {string} url - url of the repo (e.g: /dwyl/start-here)
 * @param {function} callback - callback function to be executed on success.
 */
function select_repo (url, callback) {
  connect( function select_repo_after_connected () {
    const query = `SELECT * FROM repos WHERE url = $1 ORDER BY id ASC LIMIT 1`;
    url = url.replace('/stargazers', '');
    PG_CLIENT.query(query, [url], function(error, result) {
      utils.log_error(error, result, new Error().stack);
      return utils.exec_cb(callback, error, result);
    });
  });
}

/**
 * insert_relationship saves the list of people who related to another record.
 * @param {object} data - a valid JSON object containing data to be inserted.
 * @param {function} callback - callback function to be executed on success.
 */
function insert_relationships (data, callback) {
  let fields, rel_id, url, username;
  const len = data.entries.length - 1;

  function insert_rows () { // inner function has access to outer variables
    data.entries.forEach((p, i) => { // poor person's "async parallel":
      const username = p.username;
      // console.log('username:', username);
      select_person(username, function(error1, result1) {
        // console.log('L251 > result1: ', result1.rows[0]);
        const person_id = result1.rows[0].id;
        const query = `INSERT INTO relationships (${fields}) VALUES ($1, $2)`
        const values = [person_id, rel_id];
        // console.log('query:', query, 'values:', values);
        PG_CLIENT.query(query, values, function(error2, result2) {
          utils.log_error(error2, result2, new Error().stack);

          if(i === len) {
            return insert_next_page(data, callback);
          }
        });
      });
    }); // END data.entries.forEach
  }
  // there are three types of relationships, we switch based on data.type
  switch (data.type) {
    case 'stars':
      fields = 'person_id, repo_id';
      select_repo(data.url, function (error, result) {
        rel_id = result.rows[0].id;
        insert_rows();
      }); // END select_repo
      break;
    case 'people': // this is a list of members of an organisation
      fields = 'person_id, org_id';
      url =  '/' + data.url.split('/')[2];// /orgs/dwyl/people > /dwyl
      select_org(url, function (error, result) {
        rel_id = result.rows[0].id;
        insert_rows();
      }); // END select_org
      break;
    case 'followers': // this is a list of followers/following
      fields = 'person_id, leader_id';
      username =  data.url.split('/')[1]; // /dwylbot/followers > dwylbot
      // console.log('username', username);
      // list of followers:
      select_person(username, function (error, result) {
        rel_id = result.rows[0].id;
        insert_rows();
      }); // END select_org
      break;
    case 'following': // pay attention to the subtle difference in fields order
      fields = 'leader_id, person_id';
      username =  data.url.split('/')[1]; // /dwylbot/following > dwylbot
      // console.log('username', username);
      // list of followers:
      select_person(username, function (error, result) {
        rel_id = result.rows[0].id;
        insert_rows();
      }); // END select_org
      break;
  }
}

/**
 * insert_log_item does exactly what it's name suggests inserts a log enty
 * @param {String} url - the current url (page) being viewed.
 * @param {String} next_page - the next page to be fetched.
 * @param {function} callback - callback function to be executed on success.
 */
function insert_log_item (url, next_page, callback) {
  connect( function () {
    const query = `INSERT INTO logs (url, next_page) VALUES ($1, $2)`;
    const values = [url, next_page]
    PG_CLIENT.query(query, values, function(error, data) {
      utils.log_error(error, data, new Error().stack);
      return utils.exec_cb(callback, error, data);
    });
  });
}

function profile_next_page(urls, username) {
  urls.push(username + '/followers');
  urls.push(username + '/following');
  urls.push(username + '?tab=repositories');
  return urls;
}

/**
 * insert_next_page inserts the list of next pages to be crawled.
 * @param {Object} data - a valid JSON object containing data to be inserted.
 * @param {function} callback - callback function to be executed on success.
 */
function insert_next_page (data, callback) {
  let urls = []
  switch (data.type) {
    case 'org':
      // console.log('data.name', data.name);
      urls = data.entries.map((e) => e.url);
      urls.push('orgs/' + data.name + '/people'); // list of PUBLIC org members.
      urls.push(data.next_page); // if it exists.
      break;
    case 'profile':
      urls = data.pinned.map((e) => e.url);
      const orgs = Object.keys(data.orgs);
      orgs.forEach(org => urls.push(org));
      urls = profile_next_page(urls, data.username);
      break;
    case 'repo':
      urls.push(data.url + '/stargazers');
      break;
    case 'followers':
    case 'following':
    case 'people':
    case 'stars':
      urls.push(data.next_page);
      data.entries.forEach((e) => { urls = profile_next_page(urls, e.username)})
      break;
  }
  let len = urls.length;
  urls.filter((e) => e !== null) // filter out blanks (if next_page is null)
  .forEach((next_page, i) => { // poor person's "async parallel":
    insert_log_item(data.url, next_page, (error, data2) => {
      if(--len == 0) {
        return utils.exec_cb(callback, null, data);
      }
    })
  });
}

/**
 * select_next_page get the next url (page) to crawl
 */
function select_next_page (callback) {
  connect( function () {
    const query = `SELECT next_page, COUNT (next_page) AS c
    FROM logs
    WHERE next_page IS NOT null
    AND next_page NOT IN (
      SELECT url
      FROM logs
      WHERE url IS NOT NULL
    )
    GROUP BY next_page
    ORDER BY c ASC
    LIMIT 1;`;
    // console.log('L82: query:', query);
    PG_CLIENT.query(query, function(error, data) {
      utils.log_error(error, data, new Error().stack);
      return utils.exec_cb(callback, error, data);
    });
  });
}

module.exports = {
  connect: connect,
  end: end,
  insert_log_item: insert_log_item,
  select_next_page: select_next_page,
  insert_person: insert_person,
  select_person: select_person,
  insert_org: insert_org,
  select_org: select_org,
  insert_repo: insert_repo,
  select_repo: select_repo,
  insert_relationships: insert_relationships,
  PG_CLIENT: PG_CLIENT
}


================================================
FILE: server/lanip.js
================================================
// see: https://stackoverflow.com/questions/10750303
var os = require('os');
var interfaces = os.networkInterfaces();
var ip = [];
for (var k in interfaces) {
  for (var k2 in interfaces[k]) {
    var address = interfaces[k][k2];
    if (address.family === 'IPv4' && !address.internal) {
      ip.push(address.address);
    }
  }
}
module.exports = ip[0];


================================================
FILE: server/request_handlers.js
================================================
var fs = require('fs');
var path = require('path');
// var db = require('./db.js');
var index = path.resolve(__dirname, '../client/index.html');
var app = path.resolve(__dirname, '../client/app.js');

function serve_index(req, res) {
  return fs.readFile(index, function (err, data) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end(data);
  });
}

// function serve_app(req, res) {
//   return fs.readFile(app, function (err, data) {
//     res.writeHead(200, {'Content-Type': 'text/javascript'});
//     res.end(data);
//   });
// }

module.exports = {
  serve_index: serve_index,
  // serve_app: serve_app,
  // serve_static: serve_static,
  // handle_post: handle_post,
  // handle_email_verification_request: handle_email_verification_request
}


================================================
FILE: server/server.js
================================================
process.env.PORT = process.env.PORT || 4000;
const http = require('http');
const handlers = require('./request_handlers.js');

const server = http.createServer(function run (req, res) { // can you make simplify it? ;-)
  console.log(req.method, ':', req.url);    // absolute minimum request logging
  var url = req.url.split('?')[0];          // strip query params for routing
  switch (url) {
    // case '/elmo.js':                        // not "DRY" ... #helpwanted!
    //   handlers.serve_static(req, res);
    //   break;
    // case '/app.js':                         // serve the client application
    //   handlers.serve_app(req, res);
    //   break;
    // case '/save':                           // save state to server
    //   handlers.handle_post(req, res);
    //   break;
    default:                                // serve the application
      handlers.serve_index(req, res);
      break;
  }
}).listen(process.env.PORT); // start the server with the command: npm run dev

// url used in tests:
server.url = "http://" + require('./lanip.js') + ":" + process.env.PORT;
// show local LAN IP address in console so we can connect to the app on mobile:
console.info("GOTO:", server.url);

module.exports = server;


================================================
FILE: server/utils.js
================================================
const BG = '\x1b[44m\x1b[33m\x1b[1m';
const RESET = '\x1b[0m'; // see: https://stackoverflow.com/a/41407246/1148249

/**
 * log_error is a basic error logger function which logs errors when present.
 * the beauty of centralising error logging in your apps is that it makes it
 * easy to change the logging to use a logging *service* or tool later
 * without having to change all instances of your logger.
 * @param {Object|String} error - the error reported
 * @param {Object} data - any data being passed back to the calling function.
 * @param {String} stack - the call stack where log_error was called from.
 * @example
 * utils.log_error(error, data, new Error().stack); //
 */
function log_error (error, data, stack) {
  stack = stack || 'remember to include `stack` (third param) in log_error!'
  if (error) {
    console.error(
      BG,
      'ERROR:', error, stack.toString(),
      RESET
    ); // .split('\n')[1]
  }
  return;
}

/**
 * exec_cb runs a callback if it's a function avoids type error if not a func.
 * @param {function} callback - the callback function to be executed if any.
 * @param {Object|String} error - the error reported
 * @param {Object} data - any data being passed back to the calling function.
 */
function exec_cb (callback, error, data) {
  log_error(error, data, new Error().stack);
  if (callback && typeof callback === 'function') {
    return callback(error, data);
  } // if callback is undefine or not a function do nothing!
  return;
}

/**
 * recent_activity returns the count of recent activity for a profile
 */
function recent_activity(json) {
  const DAYS = 14;
  const keys = Object.keys(json["contrib_matrix"]);
  const len = keys.length - 1;
  const latest_contribs = keys.slice(- DAYS);
  return latest_contribs.reduce((sum, k) => {
    return sum + json["contrib_matrix"][k]['count']
  }, 0);
}

module.exports = {
  log_error: log_error,
  exec_cb: exec_cb,
  recent_activity: recent_activity
}


================================================
FILE: test/bot.test.js
================================================
const tap = require('tap');
const bot = require('../server/bot');
const db = require('../server/db');
const seed = Math.floor(Math.random() * Math.floor(100000));

tap.test('crawl non-existent page to test 404', function (t) {
  bot.fetch('/totesamaze' + seed, function(err, data) {
    t.equal(err, 404, 'err: ' + err + ' (as expected ;-)');
    t.end()
  });
});

tap.test('crawl @dwyl org', function (t) {
  // we must TRUNCATE the orgs table when running tests:
  db.PG_CLIENT.query('TRUNCATE TABLE orgs CASCADE', function (err0, result0) {
    t.equal(err0, null, 'no error running "TRUNCATE TABLE orgs"');
    t.equal(result0.command, 'TRUNCATE', 'orgs table successfully truncated');

    bot.fetch('dwyl', function(err, data) {
      require('./fixtures/make-fixture')('org.json', data); // keep up-to-date
      t.end();
    });
  });
});

tap.test('crawl @iteles person profile', function (t) {
  db.PG_CLIENT.query('TRUNCATE TABLE people CASCADE', function (err0, result0) {
    t.equal(err0, null, 'no error running "TRUNCATE TABLE people"');
    t.equal(result0.command, 'TRUNCATE', 'people table successfully truncated');

    bot.fetch('iteles', function(err, data) {
      // delete(data.contrib_matrix); // TMI!
      require('./fixtures/make-fixture')('person.json', data);
      t.end()
    });

  }); // end TRUNCATE
});

tap.test('crawl dwyl/todo-list-javascript-tutorial', function (t) {
  db.PG_CLIENT.query('TRUNCATE TABLE repos CASCADE', function (err0, result0) {
    t.equal(err0, null, 'no error running "TRUNCATE TABLE repos"');
    t.equal(result0.command, 'TRUNCATE', 'repos table successfully truncated');

    bot.fetch('dwyl/todo-list-javascript-tutorial', function(err, data) {
      require('./fixtures/make-fixture')('repo.json', data);
      t.end()
    });
  }); // end TRUNCATE
});

tap.test('crawl dwyl/health', function (t) {
  // db.PG_CLIENT.query('TRUNCATE TABLE repos CASCADE', function (err0, result0) {
    // t.equal(err0, null, 'no error running "TRUNCATE TABLE repos"');
    // t.equal(result0.command, 'TRUNCATE', 'repos table successfully truncated');

    bot.fetch('dwyl/health', function(err, data) {
      require('./fixtures/make-fixture')('repo.json', data);

      const select = 'SELECT * FROM repos ORDER by id DESC LIMIT 1';
      db.PG_CLIENT.query(select, function(err, result) {
        t.equal(result.rows[0].url, data.url, 'repo.url ' + data.url);
        t.end();
      });
    });
  // }); // end TRUNCATE
});

tap.test('crawl /dwyl/health/stargazers', function (t) {
  bot.fetch('/dwyl/health/stargazers', function(err, data) {
    require('./fixtures/make-fixture')('stargazers.json', data);
    t.end()
  });
});

tap.test('crawl org members /orgs/SafeLives/people (3?)', function (t) {
  bot.fetch('/SafeLives', function(err1, data1) { // first store the org
    bot.fetch('/orgs/SafeLives/people', function(err, data) {
      require('./fixtures/make-fixture')('members.json', data);
      // console.log(data);
      t.equal(data.entries.length, 3, '/orgs/SafeLives/people has 3 people.');
      t.end()
    });
  });
});

tap.test('crawl /dwylbot/followers (expect 1)', function (t) {
  bot.fetch('/dwylbot', function(err1, data1) { // first fetch the profile
    bot.fetch('/dwylbot/followers', function(err, data) {
      require('./fixtures/make-fixture')('followers.json', data);
      // console.log(data);
      t.equal(data.entries.length, 4, '/dwylbot/following is following Simon.');
      t.end()
    });
  });
});


tap.test('crawl /dwylbot/following (expect 1)', function (t) {
  bot.fetch('/dwylbot', function(err1, data1) { // first fetch the profile
    bot.fetch('/dwylbot/following', function(err, data) {
      require('./fixtures/make-fixture')('following.json', data);
      // console.log(data);
      t.equal(data.entries.length, 1, '/dwylbot/following is following Simon.');
      t.end()
    });
  });
});

tap.test('db.end() close database connection so tests can finish', function(t) {
  db.end(function(err, data) {
    t.equal(db.PG_CLIENT._ending, true,
        'db.PG_CLIENT._ending: ' + db.PG_CLIENT._ending);
    t.end();
  });
});


================================================
FILE: test/db.test.js
================================================
process.env.DATABASE_URL = process.env.DATABASE_URL
  || "postgres://postgres:@localhost/codeface";

const tap = require('tap'); // see: github.com/dwyl/learn-tape
const db = require('../server/db');

const seed = Math.floor(Math.random() * Math.floor(100000));
const url = '/dwyl';

tap.test('db.select_next_page selects next_page to be viewed', function(t) {
  db.PG_CLIENT.query('TRUNCATE TABLE logs', function (err0, result0) {
    t.equal(err0, null, 'no error running "TRUNCATE TABLE logs"');
    t.equal(result0.command, 'TRUNCATE', 'logs table successfully truncated');

    db.insert_log_item(url, url + seed, function (err, result) {
      const select = 'SELECT * FROM logs ORDER by id DESC LIMIT 1';
      db.PG_CLIENT.query(select, function(err, result) {
        // console.log(result);
        t.equal(result.rows[0].url, url, 'logs.url is ' + url);
        t.end();
      });
    });
  });
});

tap.test('db.select_next_page selects next_page to be viewed', function(t) {
  db.select_next_page(function (err, result) {
    t.equal(result.rows[0].next_page, url + seed,
      'next_page is: ' + result.rows[0].next_page);
    t.end();
  });
});


tap.test('insert_person insert test/fixtures/person.json data', function(t) {
  const person = require('./fixtures/person.json');
  db.insert_person(person, function (err, result) {
    db.select_person(person.username, function(err, result) {
      t.equal(result.rows[0].name, person.name, 'person.name ' + person.name);
      t.end();
    });
  });
});

tap.test('insert_org', function(t) {
  const org = require('./fixtures/org.json');
  // given that we have a uniqueness constraint on the name and uid fields
  // we must TRUNCATE the orgs table when running tests:
  db.PG_CLIENT.query('TRUNCATE TABLE orgs CASCADE', function (err2, result2) {

    db.insert_org(org, function (err, result) {
      const select = 'SELECT * FROM orgs ORDER by id DESC LIMIT 1';
      db.PG_CLIENT.query(select, function(err, result) {
        t.equal(result.rows[0].uid, org.uid, 'org.uid ' + org.uid);
        t.equal(result.rows[0].name, org.name, 'org.name ' + org.name);
        t.end();
      });
    });
  });
});

tap.test('select_repo', function(t) {
  const repo = require('./fixtures/repo.json');
  db.insert_repo(repo, function (err, result) {
    db.select_repo(repo.url, function (err1, result1) {
      t.equal(result1.rows[0].url, repo.url, 'repo.url ' + repo.url);
      t.end();
    });
  });
});

tap.test('insert_relationships', function(t) {
  const stars = require('./fixtures/stargazers.json');
  db.insert_relationships(stars, function (err0, result0) { // insert all "stars"

    const repo_url = stars.url.replace('/stargazers', ''); // e.g: /dwyl/health

    db.select_repo(repo_url, function (err1, data1) {

      const repo_id = data1.rows[0].id;
      console.log('repo_id:', repo_id);
      const username = stars.entries[0].username; // e.g: SimonLab
      console.log('username:', username);

      db.select_person(username, function (err2, data2) {
        const person_id = data2.rows[0].id;
        const select = `SELECT * FROM relationships
          WHERE person_id = $1 AND repo_id = $2
          ORDER by inserted_at DESC LIMIT 1`;

        db.PG_CLIENT.query(select, [person_id, repo_id], function(err, result) {
          t.equal(result.rowCount, 1, '"stars" relationship inserted');
          t.end();
        });
      });
    });
  });
});

tap.test('db.end() close database connection so tests can finish', function(t) {
  db.end(function(err, data) {
    t.equal(db.PG_CLIENT._ending, true,
        'db.PG_CLIENT._ending: ' + db.PG_CLIENT._ending);
    t.end();
  });
});


================================================
FILE: test/fixtures/followers.json
================================================
{
  "entries": [
    {
      "avatar": "https://avatars1.githubusercontent.com/u/5723781?s=88&v=4",
      "uid": 5723781,
      "username": "melomg"
    },
    {
      "avatar": "https://avatars0.githubusercontent.com/u/15983736?s=88&v=4",
      "uid": 15983736,
      "username": "samhstn"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=88&v=4",
      "uid": 6057298,
      "username": "SimonLab"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/772937?s=88&v=4",
      "uid": 772937,
      "username": "ryanpcmcquen"
    }
  ],
  "url": "/dwylbot/followers",
  "type": "followers"
}

================================================
FILE: test/fixtures/following.json
================================================
{
  "entries": [
    {
      "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=88&v=4",
      "uid": 6057298,
      "username": "SimonLab"
    }
  ],
  "url": "/dwylbot/following",
  "type": "following"
}

================================================
FILE: test/fixtures/make-fixture.js
================================================
const fs = require('fs');
const path = require('path');

module.exports = function (filename, data) {
  filename = path.resolve('./test/fixtures/' + filename);
  fs.writeFileSync(filename, JSON.stringify(data, null, 2), 'utf8');
}


================================================
FILE: test/fixtures/members.json
================================================
{
  "entries": [
    {
      "avatar": "https://avatars0.githubusercontent.com/u/7805691?s=96&v=4",
      "uid": 7805691,
      "username": "harrygfox"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/4185328?s=96&v=4",
      "uid": 4185328,
      "username": "iteles"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=96&v=4",
      "uid": 6057298,
      "username": "SimonLab"
    }
  ],
  "url": "/orgs/SafeLives/people",
  "type": "people"
}

================================================
FILE: test/fixtures/org.json
================================================
{
  "url": "/dwyl",
  "type": "org",
  "name": "dwyl",
  "description": "Start here: https://github.com/dwyl/start-here",
  "location": "London, UK",
  "website": "https://dwyl.com",
  "email": "hello+github@dwyl.com",
  "pcount": 171,
  "avatar": "https://avatars1.githubusercontent.com/u/11708465?s=200&v=4",
  "uid": 11708465,
  "entries": [
    {
      "name": "learn-postgresql",
      "lang": "",
      "url": "/dwyl/learn-postgresql",
      "description": "🐘 Learn how to use PostgreSQL and Structured Query Language (SQL) to store and query your relational data. 🔍",
      "updated": "2019-04-15T13:09:30Z"
    },
    {
      "name": "learn-json-web-tokens",
      "lang": "JavaScript",
      "url": "/dwyl/learn-json-web-tokens",
      "description": "🔐 Learn how to use JSON Web Token (JWT) to secure your next Web App! (Tutorial/Example with Tests!!)",
      "updated": "2019-04-15T10:42:01Z"
    },
    {
      "name": "learn-hapi",
      "lang": "HTML",
      "url": "/dwyl/learn-hapi",
      "description": "☀️ Learn to use Hapi.js (Node.js) web framework to build scalable apps in less time",
      "updated": "2019-04-13T14:23:26Z"
    },
    {
      "name": "aws-sdk-mock",
      "lang": "JavaScript",
      "url": "/dwyl/aws-sdk-mock",
      "description": "🌈 AWSomocks for Javascript/Node.js aws-sdk tested, documented & maintained. Contributions welcome!",
      "updated": "2019-04-12T08:36:15Z"
    },
    {
      "name": "learn-elixir",
      "lang": "Elixir",
      "url": "/dwyl/learn-elixir",
      "description": "💧 Learn the Elixir programming language to build functional, fast, scalable and maintainable web applications!",
      "updated": "2019-04-08T07:58:17Z"
    },
    {
      "name": "hapi-error",
      "lang": "JavaScript",
      "url": "/dwyl/hapi-error",
      "description": "☔️ Intercept errors in your Hapi Web App/API and send a *useful* message to the client OR redirect to the desired endpoint.",
      "updated": "2019-04-07T12:52:07Z"
    },
    {
      "name": "home",
      "lang": "",
      "url": "/dwyl/home",
      "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!",
      "updated": "2019-03-30T22:05:51Z"
    },
    {
      "name": "elixir-dojo",
      "lang": "Elixir",
      "url": "/dwyl/elixir-dojo",
      "description": "Identify card hands",
      "updated": "2019-03-30T18:58:53Z"
    },
    {
      "name": "learn-tape",
      "lang": "JavaScript",
      "url": "/dwyl/learn-tape",
      "description": "✅ Learn how to use Tape for JavaScript/Node.js Test Driven Development (TDD) - Ten-Minute Testing Tutorial",
      "updated": "2019-03-30T13:31:23Z"
    },
    {
      "name": "learn-travis",
      "lang": "JavaScript",
      "url": "/dwyl/learn-travis",
      "description": "😎 A quick Travis CI (Continuous Integration) Tutorial for Node.js developers",
      "updated": "2019-03-30T13:15:15Z"
    },
    {
      "name": "ordem",
      "lang": "JavaScript",
      "url": "/dwyl/ordem",
      "description": "🏁 ultra-simple ordered task runner for Node.js and Browser. Run your asynchronous functions predictably in series.",
      "updated": "2019-03-29T20:26:27Z"
    },
    {
      "name": "learn-elm-architecture-in-javascript",
      "lang": "JavaScript",
      "url": "/dwyl/learn-elm-architecture-in-javascript",
      "description": "🦄 Learn how to build web apps using the Elm Architecture in \"vanilla\" JavaScript (step-by-step TDD tutorial)!",
      "updated": "2019-03-29T09:20:26Z"
    },
    {
      "name": "todo-list-javascript-tutorial",
      "lang": "JavaScript",
      "url": "/dwyl/todo-list-javascript-tutorial",
      "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. 🌱",
      "updated": "2019-03-28T21:13:46Z"
    },
    {
      "name": "process-handbook",
      "lang": "",
      "url": "/dwyl/process-handbook",
      "description": "📗 Contains our processes, questions and journey to creating ateam",
      "updated": "2019-03-27T21:49:19Z"
    },
    {
      "name": "auth",
      "lang": "Elixir",
      "url": "/dwyl/auth",
      "description": "🚪 🔐 A Complete Authentication Solution for Elixir/Phoenix Web Apps/APIs (Documented, Tested & Maintained)",
      "updated": "2019-03-26T11:50:37Z"
    },
    {
      "name": "learn-phoenix-framework",
      "lang": "Elixir",
      "url": "/dwyl/learn-phoenix-framework",
      "description": "🔥 Phoenix is the web framework without compromise on speed, reliability or maintainability! Don't settle for less. 🚀",
      "updated": "2019-03-26T11:49:30Z"
    },
    {
      "name": "phoenix-ecto-append-only-log-example",
      "lang": "Elixir",
      "url": "/dwyl/phoenix-ecto-append-only-log-example",
      "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!",
      "updated": "2019-03-25T05:50:07Z"
    },
    {
      "name": "hapi-auth-jwt2",
      "lang": "JavaScript",
      "url": "/dwyl/hapi-auth-jwt2",
      "description": "🔒 Secure Hapi.js authentication plugin using JSON Web Tokens (JWT) in Headers, Query or Cookies",
      "updated": "2019-03-24T19:51:17Z"
    },
    {
      "name": "learn-docker",
      "lang": "Dockerfile",
      "url": "/dwyl/learn-docker",
      "description": "🚢 Learn how to use docker.io containers to consistently deploy your apps on any infrastructure.",
      "updated": "2019-03-21T12:12:23Z"
    },
    {
      "name": "sendemail",
      "lang": "JavaScript",
      "url": "/dwyl/sendemail",
      "description": "✉️ Simplifies reliably sending emails from your node.js apps using AWS Simple Email Service (SES)",
      "updated": "2019-03-20T15:35:36Z"
    },
    {
      "name": "learn-heroku",
      "lang": "HTML",
      "url": "/dwyl/learn-heroku",
      "description": "🏁 Learn how to deploy your web application to Heroku from scratch step-by-step in 7 minutes!",
      "updated": "2019-03-14T11:37:37Z"
    },
    {
      "name": "learn-wireframing",
      "lang": "",
      "url": "/dwyl/learn-wireframing",
      "description": "💡 📰 Learn how to share your UX ideas with your team and the world so you can test hypotheses fast!",
      "updated": "2019-03-13T20:44:36Z"
    },
    {
      "name": "english-words",
      "lang": "Python",
      "url": "/dwyl/english-words",
      "description": "📝 A text file containing 479k English words for all your dictionary/word-based projects e.g: auto-completion / autosuggestion",
      "updated": "2019-03-13T20:35:44Z"
    },
    {
      "name": "alog",
      "lang": "Elixir",
      "url": "/dwyl/alog",
      "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).",
      "updated": "2019-03-13T11:06:36Z"
    },
    {
      "name": "phoenix-uk-postcode-finder-example",
      "lang": "Elixir",
      "url": "/dwyl/phoenix-uk-postcode-finder-example",
      "description": "📍An example/tutorial application showing how to rapidly find your nearest X by typing your postcode.",
      "updated": "2019-03-12T21:40:10Z"
    },
    {
      "name": "learn-elm",
      "lang": "HTML",
      "url": "/dwyl/learn-elm",
      "description": "🌈 discover why people are switching to Elm and how you can get started today!",
      "updated": "2019-03-11T13:58:23Z"
    },
    {
      "name": "technical-glossary",
      "lang": "",
      "url": "/dwyl/technical-glossary",
      "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! 😕 > 🤔 > 💡 > 😊 🎉 🚀",
      "updated": "2019-03-08T14:48:31Z"
    },
    {
      "name": "learn-purescript",
      "lang": "",
      "url": "/dwyl/learn-purescript",
      "description": "🚧 Learn to use Purescript to make your JavaScript Apps more reliable.",
      "updated": "2019-03-08T14:35:47Z"
    },
    {
      "name": "goodparts",
      "lang": "JavaScript",
      "url": "/dwyl/goodparts",
      "description": "🙈 An ESLint Style that only allows JavaScript the Good Parts (and \"Better Parts\") in your code.",
      "updated": "2019-03-06T14:45:23Z"
    },
    {
      "name": "product-owner-guide",
      "lang": "",
      "url": "/dwyl/product-owner-guide",
      "description": "🚀 A rough guide for people working with dwyl as Product Owners",
      "updated": "2019-03-05T15:01:05Z"
    }
  ],
  "next_page": "/dwyl?page=2"
}

================================================
FILE: test/fixtures/person.json
================================================
{
  "url": "/iteles",
  "type": "profile",
  "username": "iteles",
  "bio": "Co-founder @dwyl | Head cheerleader @foundersandcoders",
  "avatar": "https://avatars1.githubusercontent.com/u/4185328?s=400&v=4",
  "uid": 4185328,
  "repos": 28,
  "projects": 0,
  "stars": 453,
  "followers": 341,
  "following": 75,
  "pinned": [
    {
      "url": "/dwyl/start-here"
    },
    {
      "url": "/dwyl/learn-tdd"
    },
    {
      "url": "/dwyl/learn-elm-architecture-in-javascript"
    },
    {
      "url": "/dwyl/tachyons-bootstrap"
    },
    {
      "url": "/dwyl/learn-ab-and-multivariate-testing"
    },
    {
      "url": "/dwyl/learn-elixir"
    }
  ],
  "worksfor": "@dwyl",
  "location": "London, UK",
  "name": "Ines Teles Correia",
  "website": "https://www.twitter.com/iteles",
  "contribs": 869,
  "contrib_matrix": {
    "2018-04-15": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "13",
      "y": "0"
    },
    "2018-04-16": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "13",
      "y": "12"
    },
    "2018-04-17": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "13",
      "y": "24"
    },
    "2018-04-18": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "13",
      "y": "36"
    },
    "2018-04-19": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "13",
      "y": "48"
    },
    "2018-04-20": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "13",
      "y": "60"
    },
    "2018-04-21": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "13",
      "y": "72"
    },
    "2018-04-22": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "12",
      "y": "0"
    },
    "2018-04-23": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "12",
      "y": "12"
    },
    "2018-04-24": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "12",
      "y": "24"
    },
    "2018-04-25": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "12",
      "y": "36"
    },
    "2018-04-26": {
      "fill": "#239a3b",
      "count": 7,
      "x": "12",
      "y": "48"
    },
    "2018-04-27": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "12",
      "y": "60"
    },
    "2018-04-28": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "12",
      "y": "72"
    },
    "2018-04-29": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "11",
      "y": "0"
    },
    "2018-04-30": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "11",
      "y": "12"
    },
    "2018-05-01": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "11",
      "y": "24"
    },
    "2018-05-02": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "11",
      "y": "36"
    },
    "2018-05-03": {
      "fill": "#196127",
      "count": 13,
      "x": "11",
      "y": "48"
    },
    "2018-05-04": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "11",
      "y": "60"
    },
    "2018-05-05": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "11",
      "y": "72"
    },
    "2018-05-06": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "10",
      "y": "0"
    },
    "2018-05-07": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "10",
      "y": "12"
    },
    "2018-05-08": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "10",
      "y": "24"
    },
    "2018-05-09": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "10",
      "y": "36"
    },
    "2018-05-10": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "10",
      "y": "48"
    },
    "2018-05-11": {
      "fill": "#196127",
      "count": 15,
      "x": "10",
      "y": "60"
    },
    "2018-05-12": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "10",
      "y": "72"
    },
    "2018-05-13": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "9",
      "y": "0"
    },
    "2018-05-14": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "9",
      "y": "12"
    },
    "2018-05-15": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "9",
      "y": "24"
    },
    "2018-05-16": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "9",
      "y": "36"
    },
    "2018-05-17": {
      "fill": "#196127",
      "count": 9,
      "x": "9",
      "y": "48"
    },
    "2018-05-18": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "9",
      "y": "60"
    },
    "2018-05-19": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "9",
      "y": "72"
    },
    "2018-05-20": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "8",
      "y": "0"
    },
    "2018-05-21": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "8",
      "y": "12"
    },
    "2018-05-22": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "8",
      "y": "24"
    },
    "2018-05-23": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "8",
      "y": "36"
    },
    "2018-05-24": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "8",
      "y": "48"
    },
    "2018-05-25": {
      "fill": "#239a3b",
      "count": 8,
      "x": "8",
      "y": "60"
    },
    "2018-05-26": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "8",
      "y": "72"
    },
    "2018-05-27": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "7",
      "y": "0"
    },
    "2018-05-28": {
      "fill": "#239a3b",
      "count": 6,
      "x": "7",
      "y": "12"
    },
    "2018-05-29": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "7",
      "y": "24"
    },
    "2018-05-30": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "7",
      "y": "36"
    },
    "2018-05-31": {
      "fill": "#196127",
      "count": 10,
      "x": "7",
      "y": "48"
    },
    "2018-06-01": {
      "fill": "#239a3b",
      "count": 6,
      "x": "7",
      "y": "60"
    },
    "2018-06-02": {
      "fill": "#239a3b",
      "count": 6,
      "x": "7",
      "y": "72"
    },
    "2018-06-03": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "6",
      "y": "0"
    },
    "2018-06-04": {
      "fill": "#239a3b",
      "count": 8,
      "x": "6",
      "y": "12"
    },
    "2018-06-05": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "6",
      "y": "24"
    },
    "2018-06-06": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "6",
      "y": "36"
    },
    "2018-06-07": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "6",
      "y": "48"
    },
    "2018-06-08": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "6",
      "y": "60"
    },
    "2018-06-09": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "6",
      "y": "72"
    },
    "2018-06-10": {
      "fill": "#239a3b",
      "count": 6,
      "x": "5",
      "y": "0"
    },
    "2018-06-11": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "5",
      "y": "12"
    },
    "2018-06-12": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "5",
      "y": "24"
    },
    "2018-06-13": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "5",
      "y": "36"
    },
    "2018-06-14": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "5",
      "y": "48"
    },
    "2018-06-15": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "5",
      "y": "60"
    },
    "2018-06-16": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "5",
      "y": "72"
    },
    "2018-06-17": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "4",
      "y": "0"
    },
    "2018-06-18": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "4",
      "y": "12"
    },
    "2018-06-19": {
      "fill": "#239a3b",
      "count": 7,
      "x": "4",
      "y": "24"
    },
    "2018-06-20": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "4",
      "y": "36"
    },
    "2018-06-21": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "4",
      "y": "48"
    },
    "2018-06-22": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "4",
      "y": "60"
    },
    "2018-06-23": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "4",
      "y": "72"
    },
    "2018-06-24": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "3",
      "y": "0"
    },
    "2018-06-25": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "3",
      "y": "12"
    },
    "2018-06-26": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "3",
      "y": "24"
    },
    "2018-06-27": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "3",
      "y": "36"
    },
    "2018-06-28": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "3",
      "y": "48"
    },
    "2018-06-29": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "3",
      "y": "60"
    },
    "2018-06-30": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "3",
      "y": "72"
    },
    "2018-07-01": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "2",
      "y": "0"
    },
    "2018-07-02": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "2",
      "y": "12"
    },
    "2018-07-03": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "2",
      "y": "24"
    },
    "2018-07-04": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "2",
      "y": "36"
    },
    "2018-07-05": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "2",
      "y": "48"
    },
    "2018-07-06": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "2",
      "y": "60"
    },
    "2018-07-07": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "2",
      "y": "72"
    },
    "2018-07-08": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "1",
      "y": "0"
    },
    "2018-07-09": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "1",
      "y": "12"
    },
    "2018-07-10": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "1",
      "y": "24"
    },
    "2018-07-11": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "1",
      "y": "36"
    },
    "2018-07-12": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "1",
      "y": "48"
    },
    "2018-07-13": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "1",
      "y": "60"
    },
    "2018-07-14": {
      "fill": "#196127",
      "count": 9,
      "x": "1",
      "y": "72"
    },
    "2018-07-15": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "0",
      "y": "0"
    },
    "2018-07-16": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "0",
      "y": "12"
    },
    "2018-07-17": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "0",
      "y": "24"
    },
    "2018-07-18": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "0",
      "y": "36"
    },
    "2018-07-19": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "0",
      "y": "48"
    },
    "2018-07-20": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "0",
      "y": "60"
    },
    "2018-07-21": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "0",
      "y": "72"
    },
    "2018-07-22": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-1",
      "y": "0"
    },
    "2018-07-23": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-1",
      "y": "12"
    },
    "2018-07-24": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-1",
      "y": "24"
    },
    "2018-07-25": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-1",
      "y": "36"
    },
    "2018-07-26": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-1",
      "y": "48"
    },
    "2018-07-27": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-1",
      "y": "60"
    },
    "2018-07-28": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-1",
      "y": "72"
    },
    "2018-07-29": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-2",
      "y": "0"
    },
    "2018-07-30": {
      "fill": "#239a3b",
      "count": 7,
      "x": "-2",
      "y": "12"
    },
    "2018-07-31": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-2",
      "y": "24"
    },
    "2018-08-01": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-2",
      "y": "36"
    },
    "2018-08-02": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-2",
      "y": "48"
    },
    "2018-08-03": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-2",
      "y": "60"
    },
    "2018-08-04": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-2",
      "y": "72"
    },
    "2018-08-05": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-3",
      "y": "0"
    },
    "2018-08-06": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-3",
      "y": "12"
    },
    "2018-08-07": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-3",
      "y": "24"
    },
    "2018-08-08": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-3",
      "y": "36"
    },
    "2018-08-09": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-3",
      "y": "48"
    },
    "2018-08-10": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-3",
      "y": "60"
    },
    "2018-08-11": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-3",
      "y": "72"
    },
    "2018-08-12": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-4",
      "y": "0"
    },
    "2018-08-13": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-4",
      "y": "12"
    },
    "2018-08-14": {
      "fill": "#239a3b",
      "count": 7,
      "x": "-4",
      "y": "24"
    },
    "2018-08-15": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-4",
      "y": "36"
    },
    "2018-08-16": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-4",
      "y": "48"
    },
    "2018-08-17": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-4",
      "y": "60"
    },
    "2018-08-18": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-4",
      "y": "72"
    },
    "2018-08-19": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-5",
      "y": "0"
    },
    "2018-08-20": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-5",
      "y": "12"
    },
    "2018-08-21": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-5",
      "y": "24"
    },
    "2018-08-22": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-5",
      "y": "36"
    },
    "2018-08-23": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-5",
      "y": "48"
    },
    "2018-08-24": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-5",
      "y": "60"
    },
    "2018-08-25": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-5",
      "y": "72"
    },
    "2018-08-26": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-6",
      "y": "0"
    },
    "2018-08-27": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-6",
      "y": "12"
    },
    "2018-08-28": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-6",
      "y": "24"
    },
    "2018-08-29": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-6",
      "y": "36"
    },
    "2018-08-30": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-6",
      "y": "48"
    },
    "2018-08-31": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-6",
      "y": "60"
    },
    "2018-09-01": {
      "fill": "#239a3b",
      "count": 8,
      "x": "-6",
      "y": "72"
    },
    "2018-09-02": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-7",
      "y": "0"
    },
    "2018-09-03": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-7",
      "y": "12"
    },
    "2018-09-04": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-7",
      "y": "24"
    },
    "2018-09-05": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-7",
      "y": "36"
    },
    "2018-09-06": {
      "fill": "#196127",
      "count": 9,
      "x": "-7",
      "y": "48"
    },
    "2018-09-07": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-7",
      "y": "60"
    },
    "2018-09-08": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-7",
      "y": "72"
    },
    "2018-09-09": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-8",
      "y": "0"
    },
    "2018-09-10": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-8",
      "y": "12"
    },
    "2018-09-11": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-8",
      "y": "24"
    },
    "2018-09-12": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-8",
      "y": "36"
    },
    "2018-09-13": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-8",
      "y": "48"
    },
    "2018-09-14": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-8",
      "y": "60"
    },
    "2018-09-15": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-8",
      "y": "72"
    },
    "2018-09-16": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-9",
      "y": "0"
    },
    "2018-09-17": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-9",
      "y": "12"
    },
    "2018-09-18": {
      "fill": "#196127",
      "count": 9,
      "x": "-9",
      "y": "24"
    },
    "2018-09-19": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-9",
      "y": "36"
    },
    "2018-09-20": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-9",
      "y": "48"
    },
    "2018-09-21": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-9",
      "y": "60"
    },
    "2018-09-22": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-9",
      "y": "72"
    },
    "2018-09-23": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-10",
      "y": "0"
    },
    "2018-09-24": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-10",
      "y": "12"
    },
    "2018-09-25": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-10",
      "y": "24"
    },
    "2018-09-26": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-10",
      "y": "36"
    },
    "2018-09-27": {
      "fill": "#239a3b",
      "count": 7,
      "x": "-10",
      "y": "48"
    },
    "2018-09-28": {
      "fill": "#196127",
      "count": 9,
      "x": "-10",
      "y": "60"
    },
    "2018-09-29": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-10",
      "y": "72"
    },
    "2018-09-30": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-11",
      "y": "0"
    },
    "2018-10-01": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-11",
      "y": "12"
    },
    "2018-10-02": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-11",
      "y": "24"
    },
    "2018-10-03": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-11",
      "y": "36"
    },
    "2018-10-04": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-11",
      "y": "48"
    },
    "2018-10-05": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-11",
      "y": "60"
    },
    "2018-10-06": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-11",
      "y": "72"
    },
    "2018-10-07": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-12",
      "y": "0"
    },
    "2018-10-08": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-12",
      "y": "12"
    },
    "2018-10-09": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-12",
      "y": "24"
    },
    "2018-10-10": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-12",
      "y": "36"
    },
    "2018-10-11": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-12",
      "y": "48"
    },
    "2018-10-12": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-12",
      "y": "60"
    },
    "2018-10-13": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-12",
      "y": "72"
    },
    "2018-10-14": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-13",
      "y": "0"
    },
    "2018-10-15": {
      "fill": "#196127",
      "count": 9,
      "x": "-13",
      "y": "12"
    },
    "2018-10-16": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-13",
      "y": "24"
    },
    "2018-10-17": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-13",
      "y": "36"
    },
    "2018-10-18": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-13",
      "y": "48"
    },
    "2018-10-19": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-13",
      "y": "60"
    },
    "2018-10-20": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-13",
      "y": "72"
    },
    "2018-10-21": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-14",
      "y": "0"
    },
    "2018-10-22": {
      "fill": "#239a3b",
      "count": 8,
      "x": "-14",
      "y": "12"
    },
    "2018-10-23": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-14",
      "y": "24"
    },
    "2018-10-24": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-14",
      "y": "36"
    },
    "2018-10-25": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-14",
      "y": "48"
    },
    "2018-10-26": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-14",
      "y": "60"
    },
    "2018-10-27": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-14",
      "y": "72"
    },
    "2018-10-28": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-15",
      "y": "0"
    },
    "2018-10-29": {
      "fill": "#196127",
      "count": 16,
      "x": "-15",
      "y": "12"
    },
    "2018-10-30": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-15",
      "y": "24"
    },
    "2018-10-31": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-15",
      "y": "36"
    },
    "2018-11-01": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-15",
      "y": "48"
    },
    "2018-11-02": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-15",
      "y": "60"
    },
    "2018-11-03": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-15",
      "y": "72"
    },
    "2018-11-04": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-16",
      "y": "0"
    },
    "2018-11-05": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-16",
      "y": "12"
    },
    "2018-11-06": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-16",
      "y": "24"
    },
    "2018-11-07": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-16",
      "y": "36"
    },
    "2018-11-08": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-16",
      "y": "48"
    },
    "2018-11-09": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-16",
      "y": "60"
    },
    "2018-11-10": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-16",
      "y": "72"
    },
    "2018-11-11": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-17",
      "y": "0"
    },
    "2018-11-12": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-17",
      "y": "12"
    },
    "2018-11-13": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-17",
      "y": "24"
    },
    "2018-11-14": {
      "fill": "#239a3b",
      "count": 8,
      "x": "-17",
      "y": "36"
    },
    "2018-11-15": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-17",
      "y": "48"
    },
    "2018-11-16": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-17",
      "y": "60"
    },
    "2018-11-17": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-17",
      "y": "72"
    },
    "2018-11-18": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-18",
      "y": "0"
    },
    "2018-11-19": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-18",
      "y": "12"
    },
    "2018-11-20": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-18",
      "y": "24"
    },
    "2018-11-21": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-18",
      "y": "36"
    },
    "2018-11-22": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-18",
      "y": "48"
    },
    "2018-11-23": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-18",
      "y": "60"
    },
    "2018-11-24": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-18",
      "y": "72"
    },
    "2018-11-25": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-19",
      "y": "0"
    },
    "2018-11-26": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-19",
      "y": "12"
    },
    "2018-11-27": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-19",
      "y": "24"
    },
    "2018-11-28": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-19",
      "y": "36"
    },
    "2018-11-29": {
      "fill": "#196127",
      "count": 9,
      "x": "-19",
      "y": "48"
    },
    "2018-11-30": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-19",
      "y": "60"
    },
    "2018-12-01": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-19",
      "y": "72"
    },
    "2018-12-02": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-20",
      "y": "0"
    },
    "2018-12-03": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-20",
      "y": "12"
    },
    "2018-12-04": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-20",
      "y": "24"
    },
    "2018-12-05": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-20",
      "y": "36"
    },
    "2018-12-06": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-20",
      "y": "48"
    },
    "2018-12-07": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-20",
      "y": "60"
    },
    "2018-12-08": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-20",
      "y": "72"
    },
    "2018-12-09": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-21",
      "y": "0"
    },
    "2018-12-10": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-21",
      "y": "12"
    },
    "2018-12-11": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-21",
      "y": "24"
    },
    "2018-12-12": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-21",
      "y": "36"
    },
    "2018-12-13": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-21",
      "y": "48"
    },
    "2018-12-14": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-21",
      "y": "60"
    },
    "2018-12-15": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-21",
      "y": "72"
    },
    "2018-12-16": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-22",
      "y": "0"
    },
    "2018-12-17": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-22",
      "y": "12"
    },
    "2018-12-18": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-22",
      "y": "24"
    },
    "2018-12-19": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-22",
      "y": "36"
    },
    "2018-12-20": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-22",
      "y": "48"
    },
    "2018-12-21": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-22",
      "y": "60"
    },
    "2018-12-22": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-22",
      "y": "72"
    },
    "2018-12-23": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "0"
    },
    "2018-12-24": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "12"
    },
    "2018-12-25": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "24"
    },
    "2018-12-26": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "36"
    },
    "2018-12-27": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "48"
    },
    "2018-12-28": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "60"
    },
    "2018-12-29": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-23",
      "y": "72"
    },
    "2018-12-30": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-24",
      "y": "0"
    },
    "2018-12-31": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-24",
      "y": "12"
    },
    "2019-01-01": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-24",
      "y": "24"
    },
    "2019-01-02": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-24",
      "y": "36"
    },
    "2019-01-03": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-24",
      "y": "48"
    },
    "2019-01-04": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-24",
      "y": "60"
    },
    "2019-01-05": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-24",
      "y": "72"
    },
    "2019-01-06": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-25",
      "y": "0"
    },
    "2019-01-07": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-25",
      "y": "12"
    },
    "2019-01-08": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-25",
      "y": "24"
    },
    "2019-01-09": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-25",
      "y": "36"
    },
    "2019-01-10": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-25",
      "y": "48"
    },
    "2019-01-11": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-25",
      "y": "60"
    },
    "2019-01-12": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-25",
      "y": "72"
    },
    "2019-01-13": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-26",
      "y": "0"
    },
    "2019-01-14": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-26",
      "y": "12"
    },
    "2019-01-15": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-26",
      "y": "24"
    },
    "2019-01-16": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-26",
      "y": "36"
    },
    "2019-01-17": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-26",
      "y": "48"
    },
    "2019-01-18": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-26",
      "y": "60"
    },
    "2019-01-19": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-26",
      "y": "72"
    },
    "2019-01-20": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-27",
      "y": "0"
    },
    "2019-01-21": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-27",
      "y": "12"
    },
    "2019-01-22": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-27",
      "y": "24"
    },
    "2019-01-23": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-27",
      "y": "36"
    },
    "2019-01-24": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-27",
      "y": "48"
    },
    "2019-01-25": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-27",
      "y": "60"
    },
    "2019-01-26": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-27",
      "y": "72"
    },
    "2019-01-27": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-28",
      "y": "0"
    },
    "2019-01-28": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-28",
      "y": "12"
    },
    "2019-01-29": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-28",
      "y": "24"
    },
    "2019-01-30": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-28",
      "y": "36"
    },
    "2019-01-31": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-28",
      "y": "48"
    },
    "2019-02-01": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-28",
      "y": "60"
    },
    "2019-02-02": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-28",
      "y": "72"
    },
    "2019-02-03": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-29",
      "y": "0"
    },
    "2019-02-04": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-29",
      "y": "12"
    },
    "2019-02-05": {
      "fill": "#239a3b",
      "count": 7,
      "x": "-29",
      "y": "24"
    },
    "2019-02-06": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-29",
      "y": "36"
    },
    "2019-02-07": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-29",
      "y": "48"
    },
    "2019-02-08": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-29",
      "y": "60"
    },
    "2019-02-09": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-29",
      "y": "72"
    },
    "2019-02-10": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-30",
      "y": "0"
    },
    "2019-02-11": {
      "fill": "#239a3b",
      "count": 8,
      "x": "-30",
      "y": "12"
    },
    "2019-02-12": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-30",
      "y": "24"
    },
    "2019-02-13": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-30",
      "y": "36"
    },
    "2019-02-14": {
      "fill": "#239a3b",
      "count": 6,
      "x": "-30",
      "y": "48"
    },
    "2019-02-15": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-30",
      "y": "60"
    },
    "2019-02-16": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-30",
      "y": "72"
    },
    "2019-02-17": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-31",
      "y": "0"
    },
    "2019-02-18": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-31",
      "y": "12"
    },
    "2019-02-19": {
      "fill": "#196127",
      "count": 10,
      "x": "-31",
      "y": "24"
    },
    "2019-02-20": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-31",
      "y": "36"
    },
    "2019-02-21": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-31",
      "y": "48"
    },
    "2019-02-22": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-31",
      "y": "60"
    },
    "2019-02-23": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-31",
      "y": "72"
    },
    "2019-02-24": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-32",
      "y": "0"
    },
    "2019-02-25": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-32",
      "y": "12"
    },
    "2019-02-26": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-32",
      "y": "24"
    },
    "2019-02-27": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-32",
      "y": "36"
    },
    "2019-02-28": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-32",
      "y": "48"
    },
    "2019-03-01": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-32",
      "y": "60"
    },
    "2019-03-02": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-32",
      "y": "72"
    },
    "2019-03-03": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-33",
      "y": "0"
    },
    "2019-03-04": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-33",
      "y": "12"
    },
    "2019-03-05": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-33",
      "y": "24"
    },
    "2019-03-06": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-33",
      "y": "36"
    },
    "2019-03-07": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-33",
      "y": "48"
    },
    "2019-03-08": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-33",
      "y": "60"
    },
    "2019-03-09": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-33",
      "y": "72"
    },
    "2019-03-10": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-34",
      "y": "0"
    },
    "2019-03-11": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-34",
      "y": "12"
    },
    "2019-03-12": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-34",
      "y": "24"
    },
    "2019-03-13": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-34",
      "y": "36"
    },
    "2019-03-14": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-34",
      "y": "48"
    },
    "2019-03-15": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-34",
      "y": "60"
    },
    "2019-03-16": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-34",
      "y": "72"
    },
    "2019-03-17": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-35",
      "y": "0"
    },
    "2019-03-18": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-35",
      "y": "12"
    },
    "2019-03-19": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-35",
      "y": "24"
    },
    "2019-03-20": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-35",
      "y": "36"
    },
    "2019-03-21": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-35",
      "y": "48"
    },
    "2019-03-22": {
      "fill": "#196127",
      "count": 11,
      "x": "-35",
      "y": "60"
    },
    "2019-03-23": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-35",
      "y": "72"
    },
    "2019-03-24": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-36",
      "y": "0"
    },
    "2019-03-25": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-36",
      "y": "12"
    },
    "2019-03-26": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-36",
      "y": "24"
    },
    "2019-03-27": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-36",
      "y": "36"
    },
    "2019-03-28": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-36",
      "y": "48"
    },
    "2019-03-29": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-36",
      "y": "60"
    },
    "2019-03-30": {
      "fill": "#196127",
      "count": 10,
      "x": "-36",
      "y": "72"
    },
    "2019-03-31": {
      "fill": "#7bc96f",
      "count": 3,
      "x": "-37",
      "y": "0"
    },
    "2019-04-01": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-37",
      "y": "12"
    },
    "2019-04-02": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-37",
      "y": "24"
    },
    "2019-04-03": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-37",
      "y": "36"
    },
    "2019-04-04": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-37",
      "y": "48"
    },
    "2019-04-05": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-37",
      "y": "60"
    },
    "2019-04-06": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-37",
      "y": "72"
    },
    "2019-04-07": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-38",
      "y": "0"
    },
    "2019-04-08": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-38",
      "y": "12"
    },
    "2019-04-09": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-38",
      "y": "24"
    },
    "2019-04-10": {
      "fill": "#c6e48b",
      "count": 2,
      "x": "-38",
      "y": "36"
    },
    "2019-04-11": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-38",
      "y": "48"
    },
    "2019-04-12": {
      "fill": "#7bc96f",
      "count": 5,
      "x": "-38",
      "y": "60"
    },
    "2019-04-13": {
      "fill": "#ebedf0",
      "count": 0,
      "x": "-38",
      "y": "72"
    },
    "2019-04-14": {
      "fill": "#c6e48b",
      "count": 1,
      "x": "-39",
      "y": "0"
    },
    "2019-04-15": {
      "fill": "#7bc96f",
      "count": 4,
      "x": "-39",
      "y": "12"
    }
  },
  "orgs": {
    "bowlingjs": "https://avatars3.githubusercontent.com/u/8825909?s=70&v=4",
    "foundersandcoders": "https://avatars3.githubusercontent.com/u/9970257?s=70&v=4",
    "docdis": "https://avatars0.githubusercontent.com/u/10836426?s=70&v=4",
    "dwyl": "https://avatars2.githubusercontent.com/u/11708465?s=70&v=4",
    "ladiesofcode": "https://avatars0.githubusercontent.com/u/16606192?s=70&v=4",
    "TheScienceMuseum": "https://avatars0.githubusercontent.com/u/16609662?s=70&v=4",
    "SafeLives": "https://avatars2.githubusercontent.com/u/20841400?s=70&v=4"
  }
}

================================================
FILE: test/fixtures/repo.json
================================================
{
  "url": "/dwyl/health",
  "type": "repo",
  "description": "🍏 🍋 🍓 🍐 🍌 🍍 🍉 🍒",
  "website": "",
  "tags": "read, the, issues",
  "watchers": 3,
  "stars": 6,
  "forks": 0,
  "commits": 1,
  "branches": 1,
  "releases": 0,
  "langs": []
}

================================================
FILE: test/fixtures/stargazers.json
================================================
{
  "entries": [
    {
      "avatar": "https://avatars1.githubusercontent.com/u/6057298?s=88&v=4",
      "uid": 6057298,
      "username": "SimonLab"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/11595920?s=88&v=4",
      "uid": 11595920,
      "username": "rub1e"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/12380455?s=88&v=4",
      "uid": 12380455,
      "username": "bradreeder"
    },
    {
      "avatar": "https://avatars3.githubusercontent.com/u/4200487?s=88&v=4",
      "uid": 4200487,
      "username": "JoseCage"
    },
    {
      "avatar": "https://avatars1.githubusercontent.com/u/4185328?s=88&v=4",
      "uid": 4185328,
      "username": "iteles"
    },
    {
      "avatar": "https://avatars3.githubusercontent.com/u/5038030?s=88&v=4",
      "uid": 5038030,
      "username": "tunnckoCore"
    }
  ],
  "url": "/dwyl/health/stargazers",
  "type": "stars"
}

================================================
FILE: test/server.test.js
================================================
const test = require('tap').test;
const server = require('../server/server.js');
const request = require('supertest')(server.url);

test('connect to server', function (t) {
  request.get('/')
    .expect(200)
    .end(function(err, res) {
      if (err) throw err;
      server.close(function () { console.log('Server closed!'); });
      t.end()
    });
});


================================================
FILE: test/utils.test.js
================================================
const tap = require('tap'); // see: github.com/dwyl/learn-tape
const utils = require('../server/utils');

tap.test('utils.log_error', function testfn (t) {
  const error = 'DON\'T PANIC! This is only a utils.log_error test execution ☔️'
  utils.log_error(error, { "hello": "world"}, new Error().stack);
  utils.log_error(error, { "hello": "world"});
  t.end();
});

tap.test('utils.exec_cb', function(t) {
  const error = 'DON\'T PANIC! This is only a utils.exec_cb test ☔️ '
  // call without params:
  utils.exec_cb(); // no expectation but also no error!
  utils.exec_cb(function callback (e, data) {
    t.equal(e, error, 'woohoo our exec_cb works as expected!');
    t.equal(data, 'hai', 'exec_cb simply executes the callback');
    t.end();
  }, error, 'hai');
});

tap.test('utils.recent_activity', function(t) {
  const person = require('./fixtures/person.json');
  const recent_activity = utils.recent_activity(person);
  t.ok(recent_activity > 0, 'recent_activity: ' + recent_activity)
  t.end();
});


================================================
FILE: tutorial.md
================================================
# Why?

This is a self-contained tutorial for people learning
PostgreSQL for the _first_ time.



## People


psql -U postgres -d codeface -a -f schema.sql
Download .txt
gitextract__dzctz4a/

├── .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
Download .txt
SYMBOL INDEX (26 symbols across 5 files)

FILE: schema.sql
  type "people" (line 1) | CREATE TABLE IF NOT EXISTS "people" (
  type "orgs" (line 18) | CREATE TABLE IF NOT EXISTS "orgs" (
  type "repos" (line 31) | CREATE TABLE IF NOT EXISTS "repos" (
  type "logs" (line 47) | CREATE TABLE IF NOT EXISTS "logs" (
  type "relationships" (line 54) | CREATE TABLE IF NOT EXISTS "relationships" (

FILE: server/bot.js
  function fetch (line 5) | function fetch (path, callback) {
  function fetch_list_of_profiles_slowly (line 39) | function fetch_list_of_profiles_slowly (data, callback) {

FILE: server/db.js
  constant PG_CLIENT (line 5) | const PG_CLIENT = new pg.Client(process.env.DATABASE_URL);
  function connect (line 21) | function connect (callback) {
  function end (line 38) | function end (callback) {
  function insert_person (line 52) | function insert_person (data, callback) {
  function select_person (line 75) | function select_person (username, callback) {
  function insert_org (line 90) | function insert_org (data, callback) {
  function select_org (line 110) | function select_org (url, callback) {
  function insert_repo (line 126) | function insert_repo (data, callback) {
  function select_repo (line 148) | function select_repo (url, callback) {
  function insert_relationships (line 164) | function insert_relationships (data, callback) {
  function insert_log_item (line 234) | function insert_log_item (url, next_page, callback) {
  function profile_next_page (line 245) | function profile_next_page(urls, username) {
  function insert_next_page (line 257) | function insert_next_page (data, callback) {
  function select_next_page (line 297) | function select_next_page (callback) {

FILE: server/request_handlers.js
  function serve_index (line 7) | function serve_index(req, res) {

FILE: server/utils.js
  constant RESET (line 2) | const RESET = '\x1b[0m';
  function log_error (line 15) | function log_error (error, data, stack) {
  function exec_cb (line 33) | function exec_cb (callback, error, data) {
  function recent_activity (line 44) | function recent_activity(json) {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (117K chars).
[
  {
    "path": ".gitignore",
    "chars": 562,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
  },
  {
    "path": ".travis.yml",
    "chars": 216,
    "preview": "language: node_js\nnode_js:\n  - \"node\"\nenv:\n  global:\n    - DATABASE_URL=postgres://postgres:@localhost/codeface\n    - NO"
  },
  {
    "path": "FAQ.md",
    "chars": 2993,
    "preview": "\n## Why PostgreSQL and _Not_ MySQL?\n\nThe _good_ news is that almost all of your PostgreSQL knowledge\nis _directly_ trans"
  },
  {
    "path": "README.md",
    "chars": 17337,
    "preview": "<div align=\"center\">\n\n# Learn PostgreSQL\n\nLearn how to use PostgreSQL\nand Structured Query Language (SQL) to store\nand q"
  },
  {
    "path": "client/index.html",
    "chars": 692,
    "preview": "<html>\n  <head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <title>Time MVP (Title Gets "
  },
  {
    "path": "install.md",
    "chars": 90,
    "preview": "# DBeaver\n\nSee: https://github.com/dwyl/learn-postgresql/issues/43#issuecomment-469000357\n"
  },
  {
    "path": "package.json",
    "chars": 1713,
    "preview": "{\n  \"name\": \"learn-postgresql\",\n  \"version\": \"1.0.0\",\n  \"description\": \"PostgreSQL Tutorial\",\n  \"main\": \"index.js\",\n  \"d"
  },
  {
    "path": "query.sql",
    "chars": 209,
    "preview": "SELECT\n next_page,\n COUNT (next_page) AS c\nFROM\n logs\nWHERE next_page IS NOT null\nAND next_page NOT IN (\n    SELECT path"
  },
  {
    "path": "schema.sql",
    "chars": 2064,
    "preview": "CREATE TABLE IF NOT EXISTS \"people\" (\n  \"inserted_at\" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\t\"id\" SERIAL PRIMARY"
  },
  {
    "path": "server/bot.js",
    "chars": 2052,
    "preview": "const db = require('./db');\nconst utils = require('./utils');\nconst gs = require('github-scraper');\n\nfunction fetch (pat"
  },
  {
    "path": "server/db.js",
    "chars": 11838,
    "preview": "/* istanbul ignore next */\nprocess.env.DATABASE_URL = process.env.DATABASE_URL\n  || \"postgres://postgres:@localhost/code"
  },
  {
    "path": "server/lanip.js",
    "chars": 356,
    "preview": "// see: https://stackoverflow.com/questions/10750303\nvar os = require('os');\nvar interfaces = os.networkInterfaces();\nva"
  },
  {
    "path": "server/request_handlers.js",
    "chars": 769,
    "preview": "var fs = require('fs');\nvar path = require('path');\n// var db = require('./db.js');\nvar index = path.resolve(__dirname, "
  },
  {
    "path": "server/server.js",
    "chars": 1231,
    "preview": "process.env.PORT = process.env.PORT || 4000;\nconst http = require('http');\nconst handlers = require('./request_handlers."
  },
  {
    "path": "server/utils.js",
    "chars": 1953,
    "preview": "const BG = '\\x1b[44m\\x1b[33m\\x1b[1m';\nconst RESET = '\\x1b[0m'; // see: https://stackoverflow.com/a/41407246/1148249\n\n/**"
  },
  {
    "path": "test/bot.test.js",
    "chars": 4141,
    "preview": "const tap = require('tap');\nconst bot = require('../server/bot');\nconst db = require('../server/db');\nconst seed = Math."
  },
  {
    "path": "test/db.test.js",
    "chars": 3673,
    "preview": "process.env.DATABASE_URL = process.env.DATABASE_URL\n  || \"postgres://postgres:@localhost/codeface\";\n\nconst tap = require"
  },
  {
    "path": "test/fixtures/followers.json",
    "chars": 640,
    "preview": "{\n  \"entries\": [\n    {\n      \"avatar\": \"https://avatars1.githubusercontent.com/u/5723781?s=88&v=4\",\n      \"uid\": 5723781"
  },
  {
    "path": "test/fixtures/following.json",
    "chars": 216,
    "preview": "{\n  \"entries\": [\n    {\n      \"avatar\": \"https://avatars1.githubusercontent.com/u/6057298?s=88&v=4\",\n      \"uid\": 6057298"
  },
  {
    "path": "test/fixtures/make-fixture.js",
    "chars": 231,
    "preview": "const fs = require('fs');\nconst path = require('path');\n\nmodule.exports = function (filename, data) {\n  filename = path."
  },
  {
    "path": "test/fixtures/members.json",
    "chars": 498,
    "preview": "{\n  \"entries\": [\n    {\n      \"avatar\": \"https://avatars0.githubusercontent.com/u/7805691?s=96&v=4\",\n      \"uid\": 7805691"
  },
  {
    "path": "test/fixtures/org.json",
    "chars": 8779,
    "preview": "{\n  \"url\": \"/dwyl\",\n  \"type\": \"org\",\n  \"name\": \"dwyl\",\n  \"description\": \"Start here: https://github.com/dwyl/start-here\""
  },
  {
    "path": "test/fixtures/person.json",
    "chars": 39198,
    "preview": "{\n  \"url\": \"/iteles\",\n  \"type\": \"profile\",\n  \"username\": \"iteles\",\n  \"bio\": \"Co-founder @dwyl | Head cheerleader @founde"
  },
  {
    "path": "test/fixtures/repo.json",
    "chars": 239,
    "preview": "{\n  \"url\": \"/dwyl/health\",\n  \"type\": \"repo\",\n  \"description\": \"🍏 🍋 🍓 🍐 🍌 🍍 🍉 🍒\",\n  \"website\": \"\",\n  \"tags\": \"read, the, "
  },
  {
    "path": "test/fixtures/stargazers.json",
    "chars": 926,
    "preview": "{\n  \"entries\": [\n    {\n      \"avatar\": \"https://avatars1.githubusercontent.com/u/6057298?s=88&v=4\",\n      \"uid\": 6057298"
  },
  {
    "path": "test/server.test.js",
    "chars": 359,
    "preview": "const test = require('tap').test;\nconst server = require('../server/server.js');\nconst request = require('supertest')(se"
  },
  {
    "path": "test/utils.test.js",
    "chars": 1011,
    "preview": "const tap = require('tap'); // see: github.com/dwyl/learn-tape\nconst utils = require('../server/utils');\n\ntap.test('util"
  },
  {
    "path": "tutorial.md",
    "chars": 156,
    "preview": "# Why?\n\nThis is a self-contained tutorial for people learning\nPostgreSQL for the _first_ time.\n\n\n\n## People\n\n\npsql -U po"
  }
]

About this extraction

This page contains the full source code of the dwyl/learn-postgresql GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (101.7 KB), approximately 34.2k tokens, and a symbol index with 26 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!