@lexho111/plainblog 0.5.6 → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Blog.js +2 -1
- package/README.md +37 -1
- package/index.js +4 -1
- package/lexho111-plainblog-0.5.7.tgz +0 -0
- package/model/{DatabaseModel3.js → DatabaseModel.js} +2 -0
- package/model/FileAdapter.js +1 -0
- package/model/PostgresAdapter.js +44 -0
- package/model/SequelizeAdapter.js +121 -0
- package/model/SqliteAdapter.js +21 -0
- package/package.json +6 -3
- package/public/styles.min.css +67 -30
package/Blog.js
CHANGED
|
@@ -3,7 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { URLSearchParams } from "url";
|
|
5
5
|
import Article from "./Article.js";
|
|
6
|
-
import DatabaseModel from "./model/
|
|
6
|
+
import DatabaseModel from "./model/DatabaseModel.js";
|
|
7
7
|
import { fetchData, postData } from "./model/APIModel.js";
|
|
8
8
|
import { formatHTML, header, formatMarkdown, validate } from "./Formatter.js";
|
|
9
9
|
import pkg from "./package.json" with { type: "json" };
|
|
@@ -212,6 +212,7 @@ export default class Blog {
|
|
|
212
212
|
const dbTitle = await this.#databaseModel.getBlogTitle();
|
|
213
213
|
const dbArticles = await this.#databaseModel.findAll();
|
|
214
214
|
//console.log(`articles: ${JSON.stringify(dbarticles)}`)
|
|
215
|
+
console.log(`dbArticles.length: ${dbArticles.length}`)
|
|
215
216
|
if(dbArticles.length == 0) {
|
|
216
217
|
dbArticles.push(new Article("Sample Entry #1", "Prow scuttle parrel provost Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl. Swab barque interloper chantey doubloon starboard grog black jack gangway rutters.", new Date()));
|
|
217
218
|
dbArticles.push(new Article("Sample Entry #2", "Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors. Bring a spring upon her cable holystone blow the man down spanker Shiver me timbers to go on account lookout wherry doubloon chase. Belay yo-ho-ho keelhaul squiffy black spot yardarm spyglass sheet transom heave to.", new Date()));
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Plainblog
|
|
2
2
|
|
|
3
|
-
Plainblog is a simple blog generator to help you to set up and to maintain a minimalistic **single-page blog**. You can add new articles directly in the browser.
|
|
3
|
+
Plainblog is a simple blog generator to help you to set up and to maintain a minimalistic **single-page blog**. You can add new articles directly in the browser. Your data will be stored by default in a file called blog.json.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -17,6 +17,7 @@ const blog = new Blog();
|
|
|
17
17
|
blog.title = "My Blog";
|
|
18
18
|
blog.style = "body { font-family: Arial, sans-serif; } h1 { color: #333; }";
|
|
19
19
|
blog.password = "mypassword"
|
|
20
|
+
blog.init();
|
|
20
21
|
|
|
21
22
|
blog.startServer(8080);
|
|
22
23
|
```
|
|
@@ -25,6 +26,41 @@ Now you can open your blog in your webbrowser on `http://localhost:8080`. Login
|
|
|
25
26
|
|
|
26
27
|
## More Features
|
|
27
28
|
|
|
29
|
+
### set a Database Adapter
|
|
30
|
+
|
|
31
|
+
#### connect to a sqlite database
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
import { SqliteAdapter } from "@lexho111/plainblog";
|
|
35
|
+
const blog = new Blog();
|
|
36
|
+
|
|
37
|
+
const sqliteAdapter = new SqliteAdapter({
|
|
38
|
+
dbname: "blog",
|
|
39
|
+
});
|
|
40
|
+
blog.setDatabaseAdapter(sqliteAdapter);
|
|
41
|
+
|
|
42
|
+
await blog.init();
|
|
43
|
+
blog.startServer(8080);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### connect to a postgres database
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
import { PostgresAdapter } from "@lexho111/plainblog";
|
|
50
|
+
const blog = new Blog();
|
|
51
|
+
|
|
52
|
+
const postgresAdapter = new PostgresAdapter({
|
|
53
|
+
dbname: "blog",
|
|
54
|
+
username: "user",
|
|
55
|
+
password: "password",
|
|
56
|
+
host: "localhost",
|
|
57
|
+
});
|
|
58
|
+
blog.setDatabaseAdapter(postgresAdapter);
|
|
59
|
+
|
|
60
|
+
await blog.init();
|
|
61
|
+
blog.startServer(8080);
|
|
62
|
+
```
|
|
63
|
+
|
|
28
64
|
### set an API to fetch data from an external database
|
|
29
65
|
|
|
30
66
|
```
|
package/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import Blog from "./Blog.js";
|
|
2
2
|
import Article from "./Article.js";
|
|
3
|
+
import FileAdapter from "./model/FileAdapter.js";
|
|
4
|
+
import SqliteAdapter from "./model/SqliteAdapter.js";
|
|
5
|
+
import PostgresAdapter from "./model/PostgresAdapter.js";
|
|
3
6
|
|
|
4
|
-
export { Blog, Article };
|
|
7
|
+
export { Blog, Article, FileAdapter, SqliteAdapter, PostgresAdapter };
|
|
5
8
|
export default Blog;
|
|
Binary file
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import FileAdapter from "./FileAdapter.js";
|
|
2
2
|
|
|
3
3
|
export default class DatabaseModel {
|
|
4
|
+
dbtype;
|
|
4
5
|
constructor(options = {}) {
|
|
5
6
|
console.log(JSON.stringify(options));
|
|
7
|
+
this.dbtype = options.type;
|
|
6
8
|
if (options.type === "file") {
|
|
7
9
|
this.adapter = new FileAdapter(options);
|
|
8
10
|
}
|
package/model/FileAdapter.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Sequelize, DataTypes, Op } from "sequelize";
|
|
2
|
+
import SequelizeAdapter from "./SequelizeAdapter.js";
|
|
3
|
+
|
|
4
|
+
export default class PostgresAdapter extends SequelizeAdapter {
|
|
5
|
+
dbtype = "postgres";
|
|
6
|
+
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
super();
|
|
9
|
+
console.log(JSON.stringify(options));
|
|
10
|
+
this.username = options.username;
|
|
11
|
+
this.password = options.password;
|
|
12
|
+
this.host = options.host;
|
|
13
|
+
if (options.dbname) this.dbname = options.dbname;
|
|
14
|
+
if (options.dbport) this.dbport = options.dbport;
|
|
15
|
+
if (!this.username || !this.password || !this.host) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize() {
|
|
23
|
+
console.log("initialize database");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
console.log(
|
|
27
|
+
`postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
this.sequelize = new Sequelize(
|
|
31
|
+
`postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
|
|
32
|
+
{ logging: false }
|
|
33
|
+
);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.message.includes("Please install")) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
await this.initializeModels();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Sequelize, DataTypes, Op } from "sequelize";
|
|
2
|
+
|
|
3
|
+
export default class SequelizeAdapter {
|
|
4
|
+
username;
|
|
5
|
+
password;
|
|
6
|
+
host;
|
|
7
|
+
dbport = 5432;
|
|
8
|
+
dbname = "blog";
|
|
9
|
+
|
|
10
|
+
sequelize;
|
|
11
|
+
Article;
|
|
12
|
+
BlogInfo;
|
|
13
|
+
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
console.log(JSON.stringify(options));
|
|
16
|
+
|
|
17
|
+
//let Sequelize, DataTypes, Op;
|
|
18
|
+
/*try {
|
|
19
|
+
const sequelizePkg = await import("sequelize");
|
|
20
|
+
Sequelize = sequelizePkg.Sequelize;
|
|
21
|
+
DataTypes = sequelizePkg.DataTypes;
|
|
22
|
+
//Op = sequelizePkg.Op;
|
|
23
|
+
//this.#Op = Op;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Sequelize is not installed. Please install it to use PostgresAdapter: npm install sequelize"
|
|
27
|
+
);
|
|
28
|
+
}*/
|
|
29
|
+
|
|
30
|
+
// throw new Error(`Error! ${databasetype} is an unknown database type.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async initializeModels() {
|
|
34
|
+
this.Article = this.sequelize.define(
|
|
35
|
+
"Article",
|
|
36
|
+
{
|
|
37
|
+
title: DataTypes.STRING,
|
|
38
|
+
content: DataTypes.TEXT,
|
|
39
|
+
createdAt: {
|
|
40
|
+
type: DataTypes.DATE,
|
|
41
|
+
defaultValue: DataTypes.NOW,
|
|
42
|
+
},
|
|
43
|
+
updatedAt: {
|
|
44
|
+
type: DataTypes.DATE,
|
|
45
|
+
defaultValue: DataTypes.NOW,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
timestamps: true,
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
this.BlogInfo = this.sequelize.define(
|
|
54
|
+
"BlogInfo",
|
|
55
|
+
{
|
|
56
|
+
title: DataTypes.STRING,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
timestamps: false,
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// This creates the tables if they don't exist.
|
|
64
|
+
await this.sequelize.sync({ alter: true });
|
|
65
|
+
console.log("database tables synced and ready.");
|
|
66
|
+
|
|
67
|
+
// Check for and create the initial blog title right after syncing.
|
|
68
|
+
const blogInfoCount = await this.BlogInfo.count();
|
|
69
|
+
if (blogInfoCount === 0) {
|
|
70
|
+
await this.BlogInfo.create({ title: "My Default Blog Title" });
|
|
71
|
+
console.log("initialized blog title in database.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// model
|
|
76
|
+
async findAll(
|
|
77
|
+
limit = 4,
|
|
78
|
+
offset = 0,
|
|
79
|
+
startId = null,
|
|
80
|
+
endId = null,
|
|
81
|
+
order = "DESC"
|
|
82
|
+
) {
|
|
83
|
+
const where = {};
|
|
84
|
+
if (startId !== null && endId !== null) {
|
|
85
|
+
where.id = {
|
|
86
|
+
[Op.between]: [Math.min(startId, endId), Math.max(startId, endId)],
|
|
87
|
+
};
|
|
88
|
+
} else if (startId !== null) {
|
|
89
|
+
where.id = { [order === "DESC" ? Op.lte : Op.gte]: startId };
|
|
90
|
+
}
|
|
91
|
+
const options = {
|
|
92
|
+
where,
|
|
93
|
+
order: [
|
|
94
|
+
["createdAt", order],
|
|
95
|
+
["id", order],
|
|
96
|
+
],
|
|
97
|
+
limit,
|
|
98
|
+
offset,
|
|
99
|
+
};
|
|
100
|
+
const articles = await this.Article.findAll(options);
|
|
101
|
+
return articles.map((article) => article.get({ plain: true }));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async save(newArticle) {
|
|
105
|
+
await this.Article.create(newArticle);
|
|
106
|
+
console.log("Added new article:", newArticle);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getBlogTitle() {
|
|
110
|
+
// Find the first (and only) entry in the BlogInfo table.
|
|
111
|
+
const blogInfo = await this.BlogInfo.findOne();
|
|
112
|
+
|
|
113
|
+
return blogInfo.title;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async updateBlogTitle(newTitle) {
|
|
117
|
+
// Find the first (and only) entry and update its title.
|
|
118
|
+
// Using where: {} will always find the first row.
|
|
119
|
+
await this.BlogInfo.update({ title: newTitle }, { where: {} });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Sequelize } from "sequelize";
|
|
2
|
+
import SequelizeAdapter from "./SequelizeAdapter.js";
|
|
3
|
+
|
|
4
|
+
export default class SqliteAdapter extends SequelizeAdapter {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super();
|
|
7
|
+
console.log(JSON.stringify(options));
|
|
8
|
+
|
|
9
|
+
// Use the full path for the database file from the options.
|
|
10
|
+
if (options.dbname) this.dbname = options.dbname;
|
|
11
|
+
this.sequelize = new Sequelize({
|
|
12
|
+
dialect: "sqlite",
|
|
13
|
+
storage: this.dbname + ".db",
|
|
14
|
+
logging: false,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async initialize() {
|
|
19
|
+
await this.initializeModels();
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -17,13 +17,11 @@
|
|
|
17
17
|
"author": "lexho111",
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"autoprefixer": "^10.4.23",
|
|
21
20
|
"child_process": "^1.0.2",
|
|
22
21
|
"fs": "^0.0.1-security",
|
|
23
22
|
"http": "^0.0.1-security",
|
|
24
23
|
"node-fetch": "^3.3.2",
|
|
25
24
|
"path": "^0.12.7",
|
|
26
|
-
"postcss": "^8.4.35",
|
|
27
25
|
"sass": "^1.97.1",
|
|
28
26
|
"url": "^0.11.4",
|
|
29
27
|
"util": "^0.12.5"
|
|
@@ -33,5 +31,10 @@
|
|
|
33
31
|
"eslint": "^9.8.0",
|
|
34
32
|
"eslint-plugin-jest": "^28.6.0",
|
|
35
33
|
"jest": "^29.7.0"
|
|
34
|
+
},
|
|
35
|
+
"optionalDependencies": {
|
|
36
|
+
"pg": "^8.16.3",
|
|
37
|
+
"pg-hstore": "^2.3.4",
|
|
38
|
+
"sequelize": "^6.37.7"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/public/styles.min.css
CHANGED
|
@@ -1,31 +1,68 @@
|
|
|
1
|
-
body {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
body { font-family: Arial; } .grid {
|
|
2
|
+
border: 0 solid #000;
|
|
3
|
+
display: grid;
|
|
4
|
+
gap: 0.25rem;
|
|
5
|
+
grid-template-columns: 1fr;
|
|
6
|
+
}
|
|
7
|
+
.grid article {
|
|
8
|
+
border: 0 solid #ccc;
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
overflow-wrap: break-word;
|
|
12
|
+
padding: 0.25rem;
|
|
13
|
+
}
|
|
14
|
+
.grid article h2 {
|
|
15
|
+
color: rgb(53, 53, 53);
|
|
16
|
+
margin-bottom: 5px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.grid article .datetime {
|
|
20
|
+
margin: 0;
|
|
21
|
+
color: rgb(117, 117, 117);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.grid article p {
|
|
25
|
+
margin-top: 10px;
|
|
26
|
+
margin-bottom: 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
article a {
|
|
30
|
+
color: rgb(105, 105, 105);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
article a:visited {
|
|
34
|
+
color: rgb(105, 105, 105);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h1 {
|
|
38
|
+
color: #696969;
|
|
39
|
+
}
|
|
40
|
+
nav a {
|
|
41
|
+
color: #3b40c1;
|
|
42
|
+
font-size: 20px;
|
|
43
|
+
text-decoration: underline;
|
|
44
|
+
}
|
|
45
|
+
nav a:visited {
|
|
46
|
+
color: #3b40c1;
|
|
47
|
+
text-decoration-color: #3b40c1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#wrapper {
|
|
51
|
+
max-width: 500px;
|
|
52
|
+
width: 100%;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Mobile Layout (screens smaller than 1000px) */
|
|
56
|
+
@media screen and (max-width: 1000px) {
|
|
57
|
+
* {
|
|
58
|
+
font-size: 4vw;
|
|
59
|
+
}
|
|
60
|
+
#wrapper {
|
|
61
|
+
max-width: 100%;
|
|
62
|
+
width: 100%;
|
|
63
|
+
padding: 0 10px; /* Prevents text from touching the edges */
|
|
64
|
+
box-sizing: border-box;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
5
67
|
|
|
6
|
-
|
|
7
|
-
text-decoration: underline;
|
|
8
|
-
color: rgb(59, 64, 193);
|
|
9
|
-
font-size: 20px;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.datetime {
|
|
13
|
-
font-style: normal;
|
|
14
|
-
color: rgb(67, 67, 67);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
h2 {
|
|
18
|
-
margin: 0;
|
|
19
|
-
margin-bottom: 5px;
|
|
20
|
-
color: darkgray;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
p {
|
|
24
|
-
margin-top: 10px;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
span {
|
|
28
|
-
margin: 0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* source-hash: fa9deb7a7f0781f463cd3e8fd3c3ceddec518535b0a6d13af7309ef9a2f76c32 */
|
|
68
|
+
/* source-hash: bcb6644ec5b5c6f9685c9ad6c14ee551a6f908b8a2c372d3294e2d2e80d17fb7 */
|