Browse Source

Merge branch '20200324-model-modules' of zedshaw/learnjsthehardway into master

master
Zed A. Shaw 1 month ago
parent
commit
4d586b1f67
32 changed files with 490 additions and 319 deletions
  1. +18
    -5
      db/blog/index.json
  2. +34
    -26
      generator.js
  3. +21
    -5
      lib/builderator.js
  4. +3
    -3
      lib/buildrules.js
  5. +1
    -0
      lib/models.js
  6. +24
    -0
      package-lock.json
  7. +1
    -0
      package.json
  8. +3
    -0
      src/template.html
  9. +1
    -1
      static/api/blog/index.json
  10. +0
    -1
      static/api/db/modules/drawing_basics/index.json
  11. +0
    -1
      static/api/db/modules/html_basics/index.json
  12. +1
    -1
      static/api/games/0.json
  13. +1
    -1
      static/api/games/1.json
  14. +1
    -1
      static/api/slides/1.json
  15. +1
    -1
      static/api/videos/drawing_basics/01_Sight_Sized.json
  16. +31
    -0
      static/feed.atom
  17. +26
    -0
      static/feed.json
  18. +27
    -0
      static/feed.rss
  19. +23
    -23
      tests/models/auth.spec.js
  20. +50
    -49
      tests/models/user.spec.js
  21. +19
    -17
      tests/models/user_auth.spec.js
  22. +3
    -1
      tests/models/user_episode_state.spec.js
  23. +36
    -34
      tests/models/user_exercise_state.spec.js
  24. +3
    -1
      tests/models/user_module_state.spec.js
  25. +11
    -11
      tests/ui/blog.spec.js
  26. +1
    -1
      tests/ui/live.spec.js
  27. +49
    -48
      tests/ui/login.spec.js
  28. +5
    -4
      tests/ui/modules.spec.js
  29. +2
    -2
      tests/ui/phones.spec.js
  30. +83
    -82
      tests/ui/register.spec.js
  31. +6
    -0
      tools/killpg.sh
  32. +5
    -0
      tools/pgmonit.sh

+ 18
- 5
db/blog/index.json View File

@@ -1,7 +1,20 @@
{
"author":"Zed A. Shaw",
"url":"/blog/",
"title": "Learn JS The Hard Way",
"subtitle": "A blog about teaching JS on the internet.",
"posts": []
"title": "Learn JavaScript the Hard Way",
"description": "The blog for the Learn JavaScript the Hard Way course.",
"id": "https://learnjsthehardway.com/",
"link": "https://learnjsthehardway.com/",
"language": "en",
"image": "https://learnjsthehardway.com/zed_small.jpg",
"favicon": "https://learnjsthehardway.com/favicon.ico",
"copyright": "All rights reserved 2019 and beyond, Zed A. Shaw",
"generator": "LJSTHW Custom Generator",
"feedLinks": {
"json": "https://learnjsthehardway.com/feed.json",
"atom": "https://learnjsthehardway.com/feed.atom"
},
"author": {
"name": "Zed A. Shaw",
"email": "zed@learnjsthehardway.com",
"link": "https://learnjsthehardway.com/about"
}
}

+ 34
- 26
generator.js View File

@@ -9,6 +9,7 @@ const exif = require('fast-exif');
const cleaner = require('deep-cleaner');
const glob = require('glob').sync;
const path = require('path');
const Feed = require('feed').Feed;


const facts = {
@@ -19,16 +20,11 @@ const facts = {
"protected/media/video_stills/", "**/*.jpg"),
}

const inject_json = (t, s, d) => {
let data = fs.readFileSync(s).toString();
try {
let json = JSON.parse(data);
json.self = t.tail;
json.slug = "/" + t.tail_no_ext;
return JSON.stringify(json);
} catch(error) {
log.error(error, data);
}
const inject_json = (target, data) => {
return JSON.stringify({
self: target.tail,
slug:"/" + target.tail_no_ext,
...JSON.parse(data)});
}

const fix_module_base = (t) => {
@@ -41,28 +37,40 @@ const fix_module_base = (t) => {
}

const needs_blog = build.rule('blog md needs updating',
rules.md_to_html('blog', (t) => `/blog/${t.stem}`));
rules.markdown('blog', (t) => `/blog/${t.stem}`));
const needs_modules = build.rule('modules md needs updating',
rules.md_to_html('modules', fix_module_base));
rules.markdown('modules', fix_module_base));
const need_api_update = build.rule('api needs to be updated', rules.copy('api', inject_json));


const index_rollup = (src_index, target_index, contents, key) => {
let index = JSON.parse(fs.readFileSync(src_index));

index[key] = glob(contents).map(f => JSON.parse(fs.readFileSync(f)) );

build.write(target_index, JSON.stringify(index));

return index;
}


const need_blog_index = build.rule('create the blog index', {
when: f => f.blog.updated,
then: f => {
index_rollup('db/blog/index.json', 'static/api/blog/index.json',
let index = build.index_rollup('db/blog/index.json', 'static/api/blog/index.json',
'static/api/blog/posts/*.json', 'posts');

let posts = index.posts;
delete index[posts]; // clear these out for the feed generator

let feed = new Feed(index);
posts.forEach(post => {
post.url = `https://learnjsthehardway.com/blog/${post.slug}`;

feed.addItem({
title: post.metadata.title,
id: post.url,
link: post.url,
date: new Date(post.metadata.date),
description: post.metadata.summary,
content: post.metadata.summary,
author: index.author
});
});

feed.addContributor(index.author);
build.write('static/feed.json', feed.json1());
build.write('static/feed.rss', feed.rss2());
build.write('static/feed.atom', feed.atom1());
}
});

@@ -73,7 +81,7 @@ const need_modules_index = build.rule('create the modules index', {
let dir = await fs.promises.opendir('static/api/modules');
for await(let dirent of dir) {
if(dirent.isDirectory()) {
let index = index_rollup(`db/modules/${dirent.name}/index.json`,
let index = build.index_rollup(`db/modules/${dirent.name}/index.json`,
`static/api/modules/${dirent.name}/index.json`,
`static/api/modules/${dirent.name}/exercises/*.json`,
'exercises');
@@ -101,7 +109,7 @@ const need_video_stills = build.rule('video still needed', {
t.json.probe = JSON.parse(
exec(`ffprobe -v quiet -print_format json -show_format -show_streams ./${t.path}`));
// clean the probe I think
// grab the still and save it to the video still
t.target = `./protected/media/video_stills/${t.tail_no_ext}.jpg`;


+ 21
- 5
lib/builderator.js View File

@@ -1,4 +1,4 @@
const glob = require('fast-glob');
const glob = require('fast-glob').sync;
const { Rools, Rule } = require('rools');
const _ = require('lodash');
const fs = require('fs');
@@ -9,7 +9,7 @@ const Path = require('path');
const rools = new Rools({ logging: { error: true, debug: false}});

const source = (base, pattern) => {
let found = glob.sync(`${base}${pattern}`).sort();
let found = glob(`${base}${pattern}`).sort();

return found.map(f => {
let fdata = Path.parse(f);
@@ -119,14 +119,30 @@ const copy = (src, dest, filter=null) => {
mkdir(src);

if(filter) {
let data = filter(src, dest);
write(dest, data);
try {
let raw_data = fs.readFileSync(src);
let data = filter(src, dest, raw_data);
write(dest, data);
} catch (error) {
log.error(error, "Problem with filter write in copy");
}
} else {
fs.copyFileSync(src, dest);
}
}

const index_rollup = (src_index, target_index, contents, key) => {
let index = JSON.parse(fs.readFileSync(src_index));

index[key] = glob(contents).map(f => JSON.parse(fs.readFileSync(f)) );

write(target_index, JSON.stringify(index));

return index;
}


module.exports = {
rule, run, delta, without_extension,
rule, run, delta, without_extension, index_rollup,
with_extension, source, changed, from_to, mkdir, write, copy
}

+ 3
- 3
lib/buildrules.js View File

@@ -3,7 +3,7 @@ const { log } = require('./logging');
const _ = require('lodash');
const docs = require('./docs');

const md_to_html = (key, base_url_filter) => {
const markdown = (key, base_url_filter) => {
return {
when: f => build.changed(f[key]),
then: f => {
@@ -35,7 +35,7 @@ const copy = (key, filter) => {
log.debug(`Convert ${t.path} to ${t.other ? t.other.path : "COPY"} with tail ${t.tail_no_ext}`);
let dest = `static/api/${t.tail}`;
if(filter) {
build.copy(t.path, dest, (s, d) => filter(t, s, d));
build.copy(t.path, dest, (src, dest, data) => filter(t, data));
} else {
build.copy(t.path, dest);
}
@@ -48,5 +48,5 @@ const copy = (key, filter) => {


module.exports = {
md_to_html, copy
markdown, copy
}

+ 1
- 0
lib/models.js View File

@@ -215,3 +215,4 @@ exports.EpisodeState = bookshelf.model('EpisodeState',
},
);

exports.knex = knex;

+ 24
- 0
package-lock.json View File

@@ -5161,6 +5161,15 @@
"pend": "~1.2.0"
}
},
"feed": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/feed/-/feed-4.1.0.tgz",
"integrity": "sha512-dAXWXM8QMxZ1DRnAxDmy1MaWZFlh1Ku7TU3onbXgHrVJynsxkNGPUed1AxszVW8AXo43xExronVkIqK+ACsoBA==",
"dev": true,
"requires": {
"xml-js": "^1.6.11"
}
},
"figures": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
@@ -13403,6 +13412,12 @@
}
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@@ -15501,6 +15516,15 @@
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
"xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"dev": true,
"requires": {
"sax": "^1.2.4"
}
},
"xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",

+ 1
- 0
package.json View File

@@ -56,6 +56,7 @@
"faker": "^4.1.0",
"fast-exif": "^1.0.1",
"fast-glob": "^3.2.2",
"feed": "^4.1.0",
"geckodriver": "^1.19.1",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^7.0.1",

+ 3
- 0
src/template.html View File

@@ -9,6 +9,9 @@
<link rel="stylesheet" href="/css/spectre.min.css">
<link rel="stylesheet" href="/css/spectre-exp.min.css">
<link rel="stylesheet" href="/css/spectre-icons.min.css">
<link rel="alternate" type="application/rss+xml" title="RSS Feed for learnjsthehardway.com" href="/feed.rss" />
<link rel="alternate" type="application/atom+xml" title="Atom Feed for learnjsthehardway.com" href="/feed.atom" />
<link rel="alternate" type="application/json" title="JSON Feed for learnjsthehardway.com" href="/feed.json" />

%sapper.base%


+ 1
- 1
static/api/blog/index.json View File

@@ -1 +1 @@
{"author":"Zed A. Shaw","url":"/blog/","title":"Learn JS The Hard Way","subtitle":"A blog about teaching JS on the internet.","posts":[{"toc":[{"level":1,"content":"The First Real Blog Post","slug":"the-first-real-blog-post"}],"metadata":{"author":"Zed A. Shaw","date":"Mar 25, 2020","has_image":true,"summary":"A new kind of blog post for a new kind of blog.","title":"The First Real Blog Post","slug":"01-first-blog-post"}}]}
{"title":"Learn JavaScript the Hard Way","description":"The blog for the Learn JavaScript the Hard Way course.","id":"https://learnjsthehardway.com/","link":"https://learnjsthehardway.com/","language":"en","image":"https://learnjsthehardway.com/zed_small.jpg","favicon":"https://learnjsthehardway.com/favicon.ico","copyright":"All rights reserved 2019 and beyond, Zed A. Shaw","generator":"LJSTHW Custom Generator","feedLinks":{"json":"https://learnjsthehardway.com/feed.json","atom":"https://learnjsthehardway.com/feed.atom"},"author":{"name":"Zed A. Shaw","email":"zed@learnjsthehardway.com","link":"https://learnjsthehardway.com/about"},"posts":[{"toc":[{"level":1,"content":"The First Real Blog Post","slug":"the-first-real-blog-post"}],"metadata":{"author":"Zed A. Shaw","date":"Mar 25, 2020","has_image":true,"summary":"A new kind of blog post for a new kind of blog.","title":"The First Real Blog Post","slug":"01-first-blog-post"}}]}

+ 0
- 1
static/api/db/modules/drawing_basics/index.json View File

@@ -1 +0,0 @@
{"author":"Zed A. Shaw","url":"/blog/","title":"Learn JS The Hard Way","subtitle":"A blog about teaching JS on the internet.","posts":[],"exercises":[{"toc":[{"level":1,"content":"Sight Sized Drawing","slug":"sight-sized-drawing"}],"metadata":{"author":"Zed A. Shaw","date":"Mar 25, 2020","module":"/modules/drawin_basics","id":1,"title":"Sight Sized Drawing","completed":false,"image":"/images/hero-holder.svg","summary":"The first lesson where you learn about basic drawing terms and how to do sight sized drawing.","video":{"src":"/media/videos/drawing_basics/Drawing_Basics_01_Sight_Sized.webm","poster":"/media/thumbs/video_stills/400/drawing_basics/01_Sight_Sized.jpg","preload":"none"},"slug":"01-sight-sized"}}]}

+ 0
- 1
static/api/db/modules/html_basics/index.json View File

@@ -1 +0,0 @@
{"author":"Zed A. Shaw","url":"/blog/","title":"Learn JS The Hard Way","subtitle":"A blog about teaching JS on the internet.","posts":[],"exercises":[{"toc":[{"level":1,"content":"Introduction","slug":"introduction"},{"level":2,"content":"The New Design","slug":"the-new-design"},{"level":2,"content":"A Code Sample","slug":"a-code-sample"},{"level":2,"content":"TODO","slug":"todo"}],"metadata":{"author":"Zed A. Shaw","date":"Mar 25, 2020","module":"/modules/html_basics","id":2,"title":"Introduction","completed":false,"image":"/images/hero-holder.svg","summary":"This will first introduce some basic terminology for HTML.","video":{"src":"/images/sample.mp4","poster":"/images/sample.jpg","preload":"none"},"slug":"01-intro"}}]}

+ 1
- 1
static/api/games/0.json View File

@@ -1 +1 @@
{"id":0,"uuid":"","title":"Cave Game","short_description":"This is a game a made out of a book for PICO-8","tags":"#fun #games #tags","cover":"/images/400/Cave_Game.jpg","genre":"flappy bird","description":"This is a game a made out of a book for PICO-8","related":[{"url":"/words/1.json","type":"words"}],"frame_url":"/games/1.html","self":"games/0.json","slug":"/games/0"}
undefined

+ 1
- 1
static/api/games/1.json View File

@@ -1 +1 @@
{"id":1,"uuid":"","title":"Lander Game","short_description":"A little lunar lander PICO-8 game.","tags":"#fun #games #tags","cover":"/images/400/Cave_Game.jpg","genre":"flappy bird","description":"This is a game a made out of a book for PICO-8","related":[{"url":"/words/1.json","type":"words"}],"frame_url":"/games/2.html","self":"games/1.json","slug":"/games/1"}
undefined

+ 1
- 1
static/api/slides/1.json View File

@@ -1 +1 @@
{"id":1,"title":"A Test Slides","description":"This is just an idea for slides for the site","pages":["test1/SVG_Test_1.svg","test1/SVG_Test_2.svg"],"self":"slides/1.json","slug":"/slides/1"}
undefined

+ 1
- 1
static/api/videos/drawing_basics/01_Sight_Sized.json View File

@@ -1 +1 @@
{"id":10,"torrent_url":"/torrents/drawing_basics_01_sight_sized.torrent","title":"Drawing Basics #1 -- Sight Sized","short_description":"In this video I show you the basics of sight sized drawing and the basic terminology for the rest of the course.","tags":"#art #drawing","published_on":"2020","covers":{"small":"/media/video_stills/thumbs/128/drawing_basics/01_sight_sized.jpg","medium":"/media/video_stills/thumbs/400/drawing_basics/01_sight_sized.jpg","large":"/media/video_stills/thumbs/1024/drawing_basics/01_sight_sized.jpg"},"quality":"2160p","url":"/media/videos/drawing_basics/01_sight_sized.mp4","self":"videos/drawing_basics/01_Sight_Sized.json","slug":"/videos/drawing_basics/01_Sight_Sized"}
{"probe":{"streams":[{"index":0,"codec_name":"h264","codec_long_name":"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10","profile":"High","codec_type":"video","codec_time_base":"1/48","codec_tag_string":"avc1","codec_tag":"0x31637661","width":3840,"height":2160,"coded_width":3840,"coded_height":2160,"has_b_frames":2,"sample_aspect_ratio":"1:1","display_aspect_ratio":"16:9","pix_fmt":"yuv420p","level":60,"chroma_location":"left","refs":1,"is_avc":"true","nal_length_size":"4","r_frame_rate":"24/1","avg_frame_rate":"24/1","time_base":"1/12288","start_pts":0,"start_time":"0.000000","duration_ts":45864960,"duration":"3732.500000","bit_rate":"799759","bits_per_raw_sample":"8","nb_frames":"89580","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"language":"eng","handler_name":"VideoHandler","timecode":"01:00:00:00"}},{"index":1,"codec_name":"aac","codec_long_name":"AAC (Advanced Audio Coding)","profile":"LC","codec_type":"audio","codec_time_base":"1/48000","codec_tag_string":"mp4a","codec_tag":"0x6134706d","sample_fmt":"fltp","sample_rate":"48000","channels":2,"channel_layout":"stereo","bits_per_sample":0,"r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/48000","start_pts":0,"start_time":"0.000000","duration_ts":179160000,"duration":"3732.500000","bit_rate":"126959","max_bit_rate":"128000","nb_frames":"174962","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"language":"eng","handler_name":"SoundHandler"}},{"index":2,"codec_type":"data","codec_tag_string":"tmcd","codec_tag":"0x64636d74","r_frame_rate":"0/0","avg_frame_rate":"24/1","time_base":"1/12288","start_pts":0,"start_time":"0.000000","duration_ts":45864960,"duration":"3732.500000","nb_frames":"1","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"language":"eng","handler_name":"VideoHandler","timecode":"01:00:00:00"}}],"format":{"filename":"./protected/media/videos/drawing_basics/01_Sight_Sized.mp4","nb_streams":3,"nb_programs":0,"format_name":"mov,mp4,m4a,3gp,3g2,mj2","format_long_name":"QuickTime / MOV","start_time":"0.000000","duration":"3732.522000","size":"434658077","bit_rate":"931612","probe_score":100,"tags":{"major_brand":"isom","minor_version":"512","compatible_brands":"isomiso2avc1mp41","encoder":"Lavf58.29.100"}}},"slug":"drawing_basics/01_Sight_Sized.mp4","still":"/media/video_stills/drawing_basics/01_Sight_Sized.jpg","thumbs":{"128":{"size":128,"url":"/media/thumbs/128/drawing_basics/01_Sight_Sized.jpg"},"400":{"size":400,"url":"/media/thumbs/400/drawing_basics/01_Sight_Sized.jpg"},"800":{"size":800,"url":"/media/thumbs/800/drawing_basics/01_Sight_Sized.jpg"}},"description":{"id":10,"torrent_url":"/torrents/drawing_basics_01_sight_sized.torrent","title":"Drawing Basics #1 -- Sight Sized","short_description":"In this video I show you the basics of sight sized drawing and the basic terminology for the rest of the course.","tags":"#art #drawing","published_on":"2020","covers":{"small":"/media/video_stills/thumbs/128/drawing_basics/01_sight_sized.jpg","medium":"/media/video_stills/thumbs/400/drawing_basics/01_sight_sized.jpg","large":"/media/video_stills/thumbs/1024/drawing_basics/01_sight_sized.jpg"},"quality":"2160p","url":"/media/videos/drawing_basics/01_sight_sized.mp4"}}

+ 31
- 0
static/feed.atom View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://learnjsthehardway.com/</id>
<title>Learn JavaScript the Hard Way</title>
<updated>2020-04-05T15:01:45.814Z</updated>
<generator>LJSTHW Custom Generator</generator>
<author>
<name>Zed A. Shaw</name>
<email>zed@learnjsthehardway.com</email>
<uri>https://learnjsthehardway.com/about</uri>
</author>
<link rel="alternate" href="https://learnjsthehardway.com/"/>
<link rel="self" href="https://learnjsthehardway.com/feed.atom"/>
<subtitle>The blog for the Learn JavaScript the Hard Way course.</subtitle>
<logo>https://learnjsthehardway.com/zed_small.jpg</logo>
<icon>https://learnjsthehardway.com/favicon.ico</icon>
<rights>All rights reserved 2019 and beyond, Zed A. Shaw</rights>
<contributor>
<name>Zed A. Shaw</name>
<email>zed@learnjsthehardway.com</email>
<uri>https://learnjsthehardway.com/about</uri>
</contributor>
<entry>
<title type="html"><![CDATA[The First Real Blog Post]]></title>
<id>https://learnjsthehardway.com/blog/undefined</id>
<link href="https://learnjsthehardway.com/blog/undefined"/>
<updated>2020-03-25T04:00:00.000Z</updated>
<summary type="html"><![CDATA[A new kind of blog post for a new kind of blog.]]></summary>
<content type="html"><![CDATA[A new kind of blog post for a new kind of blog.]]></content>
</entry>
</feed>

+ 26
- 0
static/feed.json View File

@@ -0,0 +1,26 @@
{
"version": "https://jsonfeed.org/version/1",
"title": "Learn JavaScript the Hard Way",
"home_page_url": "https://learnjsthehardway.com/",
"feed_url": "https://learnjsthehardway.com/feed.json",
"description": "The blog for the Learn JavaScript the Hard Way course.",
"icon": "https://learnjsthehardway.com/zed_small.jpg",
"author": {
"name": "Zed A. Shaw",
"url": "https://learnjsthehardway.com/about"
},
"items": [
{
"id": "https://learnjsthehardway.com/blog/undefined",
"content_html": "A new kind of blog post for a new kind of blog.",
"url": "https://learnjsthehardway.com/blog/undefined",
"title": "The First Real Blog Post",
"summary": "A new kind of blog post for a new kind of blog.",
"date_modified": "2020-03-25T04:00:00.000Z",
"author": {
"name": "Zed A. Shaw",
"url": "https://learnjsthehardway.com/about"
}
}
]
}

+ 27
- 0
static/feed.rss View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Learn JavaScript the Hard Way</title>
<link>https://learnjsthehardway.com/</link>
<description>The blog for the Learn JavaScript the Hard Way course.</description>
<lastBuildDate>Sun, 05 Apr 2020 15:01:45 GMT</lastBuildDate>
<docs>https://validator.w3.org/feed/docs/rss2.html</docs>
<generator>LJSTHW Custom Generator</generator>
<language>en</language>
<image>
<title>Learn JavaScript the Hard Way</title>
<url>https://learnjsthehardway.com/zed_small.jpg</url>
<link>https://learnjsthehardway.com/</link>
</image>
<copyright>All rights reserved 2019 and beyond, Zed A. Shaw</copyright>
<atom:link href="https://learnjsthehardway.com/feed.atom" rel="self" type="application/rss+xml"/>
<item>
<title><![CDATA[The First Real Blog Post]]></title>
<link>https://learnjsthehardway.com/blog/undefined</link>
<guid>https://learnjsthehardway.com/blog/undefined</guid>
<pubDate>Wed, 25 Mar 2020 04:00:00 GMT</pubDate>
<description><![CDATA[A new kind of blog post for a new kind of blog.]]></description>
<content:encoded><![CDATA[A new kind of blog post for a new kind of blog.]]></content:encoded>
</item>
</channel>
</rss>

+ 23
- 23
tests/models/auth.spec.js View File

@@ -1,39 +1,39 @@
const t = require('@lib/testing');
const {User, Auth} = require('@lib/models');

const {User, Auth, knex} = require('@lib/models');

afterAll(() => {
t.close()
t.close()
knex.destroy();
});

it('Create auth works', async () => {
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', false);
expect(auth.get('email')).toBe(user.email);
expect(auth.get('token')).toBeDefined();
expect(auth.get('registration')).toBe(false);
auth.destroy();
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', false);
expect(auth.get('email')).toBe(user.email);
expect(auth.get('token')).toBeDefined();
expect(auth.get('registration')).toBe(false);
auth.destroy();
});


it('Can find by email or token', async () => {
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', true);
let found = await Auth.find_by_email(user.email);
expect(found.get("token")).toBe(auth.get("token"));
expect(found.get("email")).toBe(auth.get("email"));
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', true);
let found = await Auth.find_by_email(user.email);
expect(found.get("token")).toBe(auth.get("token"));
expect(found.get("email")).toBe(auth.get("email"));

let found2 = await Auth.find_by_token(auth.get("token"));
expect(found2.get("token")).toBe(found.get("token"));
expect(found2.get("email")).toBe(found.get("email"));
let found2 = await Auth.find_by_token(auth.get("token"));
expect(found2.get("token")).toBe(found.get("token"));
expect(found2.get("email")).toBe(found.get("email"));

found2.destroy();
found2.destroy();
});

it('Can destroy by email', async() => {
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', true);
let del = await Auth.destroy_by_email(user.email, true);
let found = await Auth.find_by_email(user.email);
expect(found).toBe(null);
let user = t.fake_person();
let auth = await Auth.create(user.email, '127.0.0.1', true);
let del = await Auth.destroy_by_email(user.email, true);
let found = await Auth.find_by_email(user.email);
expect(found).toBe(null);
});

+ 50
- 49
tests/models/user.spec.js View File

@@ -1,83 +1,84 @@
const t = require('@lib/testing');
const { User } = require('@lib/models');
const { User, knex } = require('@lib/models');

afterAll(() => {
t.close()
t.close();
knex.destroy();
});

const make_user = async () => {
const faker = t.fake_person();
let user = await User.register(faker.email, faker.name, false, true);
const faker = t.fake_person();

expect(user).toBeDefined();
expect(faker).toBeDefined();
let user = await User.register(faker.email, faker.name, false, true);

return {user, faker};
expect(user).toBeDefined();
expect(faker).toBeDefined();

return {user, faker};
}

it('Can verify is_valid', async () => {
const faker = t.fake_person();
let user = await User.forge({
email: faker.email, full_name: faker.name,
send_emails: true, registered: true, verified: true,
tos_agree: true
}).save();
let found = await User.find_and_validate(faker.email);
expect(found.user).toBeDefined();
expect(found.user.get('email')).toBe(user.get('email'));
expect(found.is_valid).toBe(true);
user.destroy();
const faker = t.fake_person();
let user = await User.forge({
email: faker.email, full_name: faker.name,
send_emails: true, registered: true, verified: true,
tos_agree: true
}).save();
let found = await User.find_and_validate(faker.email);
expect(found.user).toBeDefined();
expect(found.user.get('email')).toBe(user.get('email'));
expect(found.is_valid).toBe(true);
user.destroy();
});

it('Can delete by an email', async () => {
const faker = t.fake_person();
const faker = t.fake_person();

let user = await User.forge({
email: faker.email, full_name: faker.name,
send_emails: true, registered: true, verified: true,
tos_agree: true
}).save();
let user = await User.forge({
email: faker.email, full_name: faker.name,
send_emails: true, registered: true, verified: true,
tos_agree: true
}).save();

let res = await User.delete_by_email(faker.email, false);
expect(res).toBeDefined();
let res = await User.delete_by_email(faker.email, false);
expect(res).toBeDefined();

let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user).toBe(null);
expect(found.is_valid).toBe(false);
let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user).toBe(null);
expect(found.is_valid).toBe(false);
});

it('Can register a new user', async () => {
let {user, faker} = await make_user();
let {user, faker} = await make_user();

expect(user.get('email')).toBe(faker.email);
expect(user.get('email')).toBe(faker.email);

let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user).toBeDefined();
expect(found.user.get('email')).toBe(user.get('email'));
let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user).toBeDefined();
expect(found.user.get('email')).toBe(user.get('email'));

user.destroy();
user.destroy();
});

it('Can update a user', async () => {
let {user, faker} = await make_user();
let update = await User.update_by_email(faker.email, faker.name + ' Updated', true);
let {user, faker} = await make_user();
let update = await User.update_by_email(faker.email, faker.name + ' Updated', true);

let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user.get('full_name')).toBe(faker.name + ' Updated');
expect(found.user.get('send_emails')).toBe(true);
let found = await User.find_and_validate(faker.email);
expect(found).toBeDefined();
expect(found.user.get('full_name')).toBe(faker.name + ' Updated');
expect(found.user.get('send_emails')).toBe(true);
});


it('Can user the jsonb attributes', async () => {
let {user, faker} = await make_user();
let {user, faker} = await make_user();

user.set('finished_lesson', [1,2,3,4,5]);
user.save({method: 'update'});
user.set('finished_lesson', [1,2,3,4,5]);
user.save({method: 'update'});
});


+ 19
- 17
tests/models/user_auth.spec.js View File

@@ -1,25 +1,27 @@
const t = require('@lib/testing');
const {User, Auth} = require('@lib/models');
const {User, Auth, knex} = require('@lib/models');

afterAll(() => knex.destroy() );

it('Can reference a user', async() => {
let faker = t.fake_person();
let user = await User.register(faker.email, faker.name, false, true);
let auth = await Auth.forge({email: faker.email, ip_addr: '127.0.0.1', registration: false, user_id: user.id, token: 'asdfasdfasdf'}).save();
let faker = t.fake_person();
let user = await User.register(faker.email, faker.name, false, true);
let auth = await Auth.forge({email: faker.email, ip_addr: '127.0.0.1', registration: false, user_id: user.id, token: 'asdfasdfasdf'}).save();

// User has many Auth
let user2 = await User.forge({id: user.id}).fetch({withRelated: 'auths'});
let auth2 = await user2.related('auths');
expect(user2).toBeDefined();
expect(await auth2.count()).toBe(1);
auth2 = await auth2.fetchOne(); // whoops, i broke the law
expect(auth2.id).toBeDefined();
// User has many Auth
let user2 = await User.forge({id: user.id}).fetch({withRelated: 'auths'});
let auth2 = await user2.related('auths');
expect(user2).toBeDefined();
expect(await auth2.count()).toBe(1);
auth2 = await auth2.fetchOne(); // whoops, i broke the law
expect(auth2.id).toBeDefined();

// Auth has one User
let auth3 = await Auth.forge({id: auth.id}).fetch({withRelated: 'user'});
expect(auth3.id).toBeDefined();
expect(auth3.id).toBe(auth2.id);
// Auth has one User
let auth3 = await Auth.forge({id: auth.id}).fetch({withRelated: 'user'});
expect(auth3.id).toBeDefined();
expect(auth3.id).toBe(auth2.id);

let user3 = await auth3.related('user');
expect(user3.id).toBe(user2.id);
let user3 = await auth3.related('user');
expect(user3.id).toBe(user2.id);
});


+ 3
- 1
tests/models/user_episode_state.spec.js View File

@@ -1,5 +1,7 @@
const t = require('@lib/testing');
const {User, EpisodeState} = require('@lib/models');
const {User, EpisodeState, knex} = require('@lib/models');

afterAll(() => knex.destroy() );

it('Can reference a user', async() => {
let faker = t.fake_person();

+ 36
- 34
tests/models/user_exercise_state.spec.js View File

@@ -1,39 +1,41 @@
const t = require('@lib/testing');
const {User, ExerciseState, ModuleState} = require('@lib/models');
const {User, ExerciseState, ModuleState, knex} = require('@lib/models');

afterAll(() => knex.destroy() );

it('Can reference a user', async() => {
let faker = t.fake_person();
let user = await User.register(faker.email, faker.name, false, true);
expect(user).toBeDefined();
let mod = await ModuleState.forge({user_id: user.id, url: '/modules/test'}).save();
expect(mod).toBeDefined();
expect(mod.id).toBeDefined();
let ex1 = await ExerciseState.forge({
user_id: user.id,
module_state_id: mod.id,
url: '/modules/test/1-ex1',
}).save();
expect(ex1).toBeDefined();
expect(ex1.id).toBeDefined();
expect(ex1.get('url')).toBeDefined();
// go from user to episode states
let u2 = await User.where({id: user.id}).fetch();
expect(u2).toBeDefined();
expect(u2.id).toBeDefined();
expect(u2.id).toBe(user.id);
let ex2 = await u2.related('exercise_states').fetchOne();
expect(ex2).toBeDefined();
expect(ex2.id).toBeDefined();
expect(ex2.id).toBe(ex1.id);
// go from exisode to user
let u3 = await ex2.related('user').fetch();
expect(u3).toBeDefined();
expect(u3.id).toBeDefined();
expect(u3.id).toBe(u2.id);
let faker = t.fake_person();
let user = await User.register(faker.email, faker.name, false, true);
expect(user).toBeDefined();
let mod = await ModuleState.forge({user_id: user.id, url: '/modules/test'}).save();
expect(mod).toBeDefined();
expect(mod.id).toBeDefined();
let ex1 = await ExerciseState.forge({
user_id: user.id,
module_state_id: mod.id,
url: '/modules/test/1-ex1',
}).save();
expect(ex1).toBeDefined();
expect(ex1.id).toBeDefined();
expect(ex1.get('url')).toBeDefined();
// go from user to episode states
let u2 = await User.where({id: user.id}).fetch();
expect(u2).toBeDefined();
expect(u2.id).toBeDefined();
expect(u2.id).toBe(user.id);
let ex2 = await u2.related('exercise_states').fetchOne();
expect(ex2).toBeDefined();
expect(ex2.id).toBeDefined();
expect(ex2.id).toBe(ex1.id);
// go from exisode to user
let u3 = await ex2.related('user').fetch();
expect(u3).toBeDefined();
expect(u3.id).toBeDefined();
expect(u3.id).toBe(u2.id);
});


+ 3
- 1
tests/models/user_module_state.spec.js View File

@@ -1,5 +1,7 @@
const t = require('@lib/testing');
const {User, ModuleState, ExerciseState} = require('@lib/models');
const {User, ModuleState, ExerciseState, knex} = require('@lib/models');

afterAll(() => knex.destroy() );

it('Can reference a user', async() => {
let faker = t.fake_person();

+ 11
- 11
tests/ui/blog.spec.js View File

@@ -8,18 +8,18 @@ const host = 'http://localhost:3000/';
afterAll(() => t.close());

it('Can list and view blog posts', async () => {
let [browser, page] = await t.begin(host);
let blog = t.sel('nav-blog-icon');
await t.sleep(500);
let [browser, page] = await t.begin(host);
let blog = t.sel('nav-blog-icon');
await t.sleep(500);

console.log("Waiting for the blog...");
t.sleep(5000);
await page.waitForSelector(blog);
console.log("Got the blog. Clicking...");
await page.click(blog);
console.log("Waiting for the blog...");
t.sleep(5000);
await page.waitForSelector(blog);
console.log("Got the blog. Clicking...");
await page.click(blog);

await t.has_content(page, t.sel('blog-summary'), 'blog post');
await t.has_content(page, t.sel('blog-summary'), 'blog post');

await page.click(t.sel('blog-post'));
await t.has_content(page, t.sel('blog-content'), 'The First Real');
await page.click(t.sel('blog-post'));
await t.has_content(page, t.sel('blog-content'), 'The First Real');
});

+ 1
- 1
tests/ui/live.spec.js View File

@@ -3,7 +3,7 @@ const t = require('@lib/testing');
const host = 'http://localhost:3000/';

afterAll(() => {
t.close()
t.close()
});

it('Can watch the live stream after registering.', async () => {

+ 49
- 48
tests/ui/login.spec.js View File

@@ -1,62 +1,63 @@
const faker = require('faker');
const t = require('@lib/testing');
const { User, Auth } = require('@lib/models');
const { User, Auth, knex} = require('@lib/models');

const main_user = t.fake_person();

const host = 'http://localhost:3000/';

afterAll(async () => {
t.close()
await User.delete_by_email(main_user.email, false);
t.close()
await User.delete_by_email(main_user.email, false);
knex.destroy();
});

it('Can log people in and out', async () => {
let [browser, page] = await t.begin(host);
await t.sleep(500);
await page.waitForSelector(t.sel('nav-login'));
await page.click(t.sel('nav-login'));
// wait for the login page to show up then submit email
await page.waitForSelector(t.sel('email'));
// first try a bad email and check for the error
await page.type(t.sel('email'), main_user.email[0]);
await page.click(t.sel('email-button'));
await t.has_content(page, t.sel('error'), 'not a valid email');
// then send a real one, use the trick of type first char then rest
// RAT: puppeteer can't clear inputs so we type 1 char, fail, then the rest
await page.type(t.sel('email'), main_user.email.substring(1));
await page.click(t.sel('email-button'));
// make sure that they get told they were sent an email
await t.has_content(page, t.sel('email-sent-msg'), "You've been sent");
// go in through the backend and get the token
let { user, is_valid } = await User.find_and_validate(main_user.email);
expect(is_valid).toBe(false);
// now submit it pretending we got it over an email
await page.waitForSelector(t.sel('token'));
let token = await Auth.token_by_email(main_user.email);
expect(token).toBeDefined();
await page.type(t.sel('token'), token[0]);
await page.click(t.sel('token-button'));
// attempt to fail at the verify step
await t.has_content(page, t.sel('error'), 'Expected auth not found');
// now do the real one
await page.type(t.sel('token'), token.substring(1));
await page.click(t.sel('token-button'));
await t.sleep(500);
await t.has_content(page, t.sel('register-page'), "Please Register");
// now log them out to test the full round trip
await page.click(t.sel('nav-logout'));
t.sleep(1000);
return true;
let [browser, page] = await t.begin(host);
await t.sleep(500);
await page.waitForSelector(t.sel('nav-login'));
await page.click(t.sel('nav-login'));
// wait for the login page to show up then submit email
await page.waitForSelector(t.sel('email'));
// first try a bad email and check for the error
await page.type(t.sel('email'), main_user.email[0]);
await page.click(t.sel('email-button'));
await t.has_content(page, t.sel('error'), 'not a valid email');
// then send a real one, use the trick of type first char then rest
// RAT: puppeteer can't clear inputs so we type 1 char, fail, then the rest
await page.type(t.sel('email'), main_user.email.substring(1));
await page.click(t.sel('email-button'));
// make sure that they get told they were sent an email
await t.has_content(page, t.sel('email-sent-msg'), "You've been sent");
// go in through the backend and get the token
let { user, is_valid } = await User.find_and_validate(main_user.email);
expect(is_valid).toBe(false);
// now submit it pretending we got it over an email
await page.waitForSelector(t.sel('token'));
let token = await Auth.token_by_email(main_user.email);
expect(token).toBeDefined();
await page.type(t.sel('token'), token[0]);
await page.click(t.sel('token-button'));
// attempt to fail at the verify step
await t.has_content(page, t.sel('error'), 'Expected auth not found');
// now do the real one
await page.type(t.sel('token'), token.substring(1));
await page.click(t.sel('token-button'));
await t.sleep(500);
await t.has_content(page, t.sel('register-page'), "Please Register");
// now log them out to test the full round trip
await page.click(t.sel('nav-logout'));
t.sleep(1000);
return true;
});



+ 5
- 4
tests/ui/modules.spec.js View File

@@ -1,16 +1,17 @@
const t = require('@lib/testing');
const {User, Auth} = require('@lib/models');
const {User, Auth, knex} = require('@lib/models');

const main_user = t.fake_person();

const host = 'http://localhost:3000/';

afterAll(async () => {
t.close()
await User.delete_by_email(main_user.email, false);
t.close();
await User.delete_by_email(main_user.email, false);
knex.destroy();
});

it('Can list modules and access one', async () => {
let [browser, page] = await t.begin(host + 'modules/');
let [browser, page] = await t.begin(host + 'modules/');
});


+ 2
- 2
tests/ui/phones.spec.js View File

@@ -2,11 +2,11 @@ const t = require('@lib/testing');
const host = 'http://localhost:3000';

it("iPhoneX doesn't look terrible.", async () => {
t.device_check(host, 'iPhone X');
t.device_check(host, 'iPhone X');
});

it("Pixel 2 doesn't look terrible", async () => {
t.device_check(host, 'Pixel 2');
t.device_check(host, 'Pixel 2');
});



+ 83
- 82
tests/ui/register.spec.js View File

@@ -1,97 +1,98 @@

const t = require('@lib/testing');
const {User, Auth} = require('@lib/models');
const {User, Auth, knex} = require('@lib/models');

const main_user = t.fake_person();

const host = 'http://localhost:3000/';

afterAll(() => {
t.close()
t.close();
knex.destroy();
});

it('Can register for an account after soft login', async () => {
let [browser, page] = await t.begin(host + 'auth/');
await t.sleep(500);
// wait for the login page to show up then submit email
await page.waitForSelector(t.sel('email'));
await page.type(t.sel('email'), main_user.email);
await page.click(t.sel('email-button'));
// make sure that they get told they were sent an email
await t.sleep(2000);
await t.has_content(page, t.sel('email-sent-msg'), "You've been sent");
// go in through the backend and get the token
let token = await Auth.token_by_email(main_user.email);
expect(token).toBeDefined();
// now submit it pretending we got it over an email
await page.waitForSelector(t.sel('token'));
// submit the token to log in
await page.type(t.sel('token'), token);
await page.click(t.sel('token-button'));
// now they are asked to register since this is a new account
await t.has_content(page, t.sel('register-page'), "Please Register");
// now fill out the registration and confirm it worked
await page.type(t.sel('full-name'), main_user.name);
await page.waitForSelector(t.sel('tos-agree'));
await page.click(t.sel('send-emails'));
await page.click(t.sel('tos-agree'));
await page.click(t.sel('register-submit'));
await page.waitForSelector(t.sel('modules-page'));
// and validate the user in the backend
let {user, is_valid} = await User.find_and_validate(main_user.email, true);
expect(is_valid).toBe(true);
expect(user.email).toEqual(main_user.email);
expect(user.registered).toBe(true);
expect(user.send_emails).toBe(true);
expect(user.verified).toBe(true);
expect(user.full_name).toEqual(main_user.name);
// it's just a lot easier to put this here
// test user can update their information
await page.click(t.sel('nav-user'));
await page.waitForSelector(t.sel('full-name'));
// just add on since puppet doesn't properly select fields
await page.type(t.sel('full-name'), ' Updated');
await page.click(t.sel('send-emails'));
await page.click(t.sel('account-submit'));
let updated = await User.find_and_validate(main_user.email, false);
while(updated.user.get('send_emails') == true) {
console.log("User hasn't been set yet.", updated.user.full_name);
await t.sleep(1000); /// so dumb, but have to wait for this to finish
updated = await User.find_and_validate(main_user.email, false);
}
expect(updated.user).toBeDefined();
expect(updated.is_valid).toBe(true);
expect(updated.user.get('full_name')).toBe(main_user.name + ' Updated');
expect(updated.user.get('send_emails')).toBe(false);
let [browser, page] = await t.begin(host + 'auth/');
await t.sleep(500);
// wait for the login page to show up then submit email
await page.waitForSelector(t.sel('email'));
await page.type(t.sel('email'), main_user.email);
await page.click(t.sel('email-button'));
// make sure that they get told they were sent an email
await t.sleep(2000);
await t.has_content(page, t.sel('email-sent-msg'), "You've been sent");
// go in through the backend and get the token
let token = await Auth.token_by_email(main_user.email);
expect(token).toBeDefined();
// now submit it pretending we got it over an email
await page.waitForSelector(t.sel('token'));
// submit the token to log in
await page.type(t.sel('token'), token);
await page.click(t.sel('token-button'));
// now they are asked to register since this is a new account
await t.has_content(page, t.sel('register-page'), "Please Register");
// now fill out the registration and confirm it worked
await page.type(t.sel('full-name'), main_user.name);
await page.waitForSelector(t.sel('tos-agree'));
await page.click(t.sel('send-emails'));
await page.click(t.sel('tos-agree'));
await page.click(t.sel('register-submit'));
await page.waitForSelector(t.sel('modules-page'));
// and validate the user in the backend
let {user, is_valid} = await User.find_and_validate(main_user.email, true);
expect(is_valid).toBe(true);
expect(user.email).toEqual(main_user.email);
expect(user.registered).toBe(true);
expect(user.send_emails).toBe(true);
expect(user.verified).toBe(true);
expect(user.full_name).toEqual(main_user.name);
// it's just a lot easier to put this here
// test user can update their information
await page.click(t.sel('nav-user'));
await page.waitForSelector(t.sel('full-name'));
// just add on since puppet doesn't properly select fields
await page.type(t.sel('full-name'), ' Updated');
await page.click(t.sel('send-emails'));
await page.click(t.sel('account-submit'));
let updated = await User.find_and_validate(main_user.email, false);
while(updated.user.get('send_emails') == true) {
console.log("User hasn't been set yet.", updated.user.full_name);
await t.sleep(1000); /// so dumb, but have to wait for this to finish
updated = await User.find_and_validate(main_user.email, false);
}
expect(updated.user).toBeDefined();
expect(updated.is_valid).toBe(true);
expect(updated.user.get('full_name')).toBe(main_user.name + ' Updated');
expect(updated.user.get('send_emails')).toBe(false);
});

it('Can register off the index.', async () => {
let [browser, page] = await t.begin(host);
const user2 = t.fake_person();
await t.sleep(100);
await page.waitForSelector(t.sel('nav-logout'));
await page.click(t.sel('nav-logout'));
await t.sleep(100);
await page.waitForSelector(t.sel('register-input'));
await page.type(t.sel('register-input'), user2.email);
await page.click(t.sel('register-submit'));
await t.has_content(page, t.sel('login-page'), "Login or Sign Up");
let [browser, page] = await t.begin(host);
const user2 = t.fake_person();
await t.sleep(100);
await page.waitForSelector(t.sel('nav-logout'));
await page.click(t.sel('nav-logout'));
await t.sleep(100);
await page.waitForSelector(t.sel('register-input'));
await page.type(t.sel('register-input'), user2.email);
await page.click(t.sel('register-submit'));
await t.has_content(page, t.sel('login-page'), "Login or Sign Up");
});

+ 6
- 0
tools/killpg.sh View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash

set -e

psql -c "select count(*) from pg_stat_activity;"
psql -c "select pg_terminate_backend(pid) from pg_stat_activity where pid <> pg_backend_pid();"

+ 5
- 0
tools/pgmonit.sh View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e

psql -c "select usename, client_addr, client_port, backend_start, query_start from pg_stat_activity;"
psql -c "select count(*) from pg_stat_activity;"

Loading…
Cancel
Save