@lexho111/plainblog 0.5.7 → 0.5.9

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 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/DatabaseModel3.js";
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" };
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 two file called _bloginfo.json_ and _articles.txt_.
4
4
 
5
5
  ## Installation
6
6
 
@@ -26,6 +26,41 @@ Now you can open your blog in your webbrowser on `http://localhost:8080`. Login
26
26
 
27
27
  ## More Features
28
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
+
29
64
  ### set an API to fetch data from an external database
30
65
 
31
66
  ```
package/articles.txt ADDED
@@ -0,0 +1,3 @@
1
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:19:39.939Z"}
2
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:34:38.866Z"}
3
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:43:32.343Z"}
package/blog.db CHANGED
Binary file
package/bloginfo.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "title": "Blog"
3
+ }
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
  }
@@ -1,12 +1,21 @@
1
- import { save as saveToFile, load as loadFromFile } from "./FileModel.js";
1
+ import {
2
+ saveInfo,
3
+ loadInfo,
4
+ appendArticle,
5
+ loadArticles,
6
+ initFiles,
7
+ } from "./FileModel.js";
2
8
 
3
9
  export default class FileAdapter {
10
+ dbtype = "file";
4
11
  constructor(options) {
5
- this.filename = "blog.json";
12
+ this.infoFile = "bloginfo.json";
13
+ this.articlesFile = "articles.txt";
6
14
  }
7
15
 
8
16
  async initialize() {
9
17
  console.log("file adapter init");
18
+ await initFiles(this.infoFile, this.articlesFile);
10
19
  }
11
20
 
12
21
  test() {
@@ -14,42 +23,19 @@ export default class FileAdapter {
14
23
  }
15
24
 
16
25
  async getBlogTitle() {
17
- const blogTitle = "TestBlog";
18
- return new Promise((res, rej) => {
19
- res(blogTitle);
20
- });
26
+ const info = await loadInfo(this.infoFile);
27
+ return info.title || "Blog";
21
28
  }
22
29
 
23
30
  async save(newArticle) {
24
31
  if (!newArticle.createdAt) {
25
32
  newArticle.createdAt = new Date().toISOString();
26
33
  }
27
-
28
- let blogTitle = "";
29
- let articles = [];
30
- try {
31
- await loadFromFile(this.filename, (t, a) => {
32
- blogTitle = t;
33
- articles = a || [];
34
- });
35
- } catch (err) {
36
- console.error(err);
37
- }
38
-
39
- articles.push(newArticle);
40
- saveToFile(this.filename, { title: blogTitle, articles });
34
+ await appendArticle(this.articlesFile, newArticle);
41
35
  }
42
36
 
43
37
  async updateBlogTitle(newTitle) {
44
- let articles = [];
45
- try {
46
- await loadFromFile(this.filename, (t, a) => {
47
- articles = a || [];
48
- });
49
- } catch (err) {
50
- console.error(err);
51
- }
52
- saveToFile(this.filename, { title: newTitle, articles });
38
+ await saveInfo(this.infoFile, { title: newTitle });
53
39
  }
54
40
 
55
41
  async findAll(
@@ -61,16 +47,15 @@ export default class FileAdapter {
61
47
  ) {
62
48
  let dbArticles = [];
63
49
  try {
64
- await loadFromFile(this.filename, (title, articles) => {
65
- if (Array.isArray(articles)) {
66
- articles.sort((a, b) => {
67
- const dateA = new Date(a.createdAt || 0);
68
- const dateB = new Date(b.createdAt || 0);
69
- return order === "DESC" ? dateB - dateA : dateA - dateB;
70
- });
71
- dbArticles = articles.slice(offset, offset + limit);
72
- }
73
- });
50
+ const articles = await loadArticles(this.articlesFile);
51
+ if (Array.isArray(articles)) {
52
+ articles.sort((a, b) => {
53
+ const dateA = new Date(a.createdAt || 0);
54
+ const dateB = new Date(b.createdAt || 0);
55
+ return order === "DESC" ? dateB - dateA : dateA - dateB;
56
+ });
57
+ dbArticles = articles.slice(offset, offset + limit);
58
+ }
74
59
  } catch (err) {
75
60
  console.error(err);
76
61
  }
@@ -1,35 +1,49 @@
1
1
  import { promises as fs } from "fs";
2
2
 
3
- /** save blog content to file */
4
- export async function save(filename, data) {
5
- if (!filename) {
6
- console.error("Error: Filename not provided and not set previously.");
7
- return;
8
- }
3
+ /** save blog info (title, etc) to a standard JSON file */
4
+ export async function saveInfo(filename, data) {
5
+ await fs.writeFile(filename, JSON.stringify(data, null, 2));
6
+ }
9
7
 
8
+ /** load blog info */
9
+ export async function loadInfo(filename) {
10
10
  try {
11
- await fs.writeFile(filename, JSON.stringify(data, null, 2));
12
- console.log(`Blog data saved to ${filename}`);
11
+ const data = await fs.readFile(filename, "utf8");
12
+ return JSON.parse(data);
13
13
  } catch (err) {
14
- console.error("Error saving blog data:", err);
14
+ return { title: "Blog" };
15
15
  }
16
16
  }
17
17
 
18
- /** load blog content from file */
19
- export async function load(filename, f) {
18
+ /** append an article as a new line to the file */
19
+ export async function appendArticle(filename, article) {
20
+ await fs.appendFile(filename, JSON.stringify(article) + "\n");
21
+ }
22
+
23
+ /** load all articles by reading line by line */
24
+ export async function loadArticles(filename) {
20
25
  try {
21
26
  const data = await fs.readFile(filename, "utf8");
22
- const jsonData = JSON.parse(data);
23
- const title = jsonData.title;
24
- const articles = jsonData.articles;
25
- f(title, articles);
27
+ return data
28
+ .split("\n")
29
+ .filter((line) => line.trim() !== "")
30
+ .map((line) => JSON.parse(line));
31
+ } catch (err) {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ /** ensure files exist */
37
+ export async function initFiles(infoFilename, articlesFilename) {
38
+ try {
39
+ await fs.access(infoFilename);
40
+ } catch (err) {
41
+ await saveInfo(infoFilename, { title: "Blog" });
42
+ }
43
+
44
+ try {
45
+ await fs.access(articlesFilename);
26
46
  } catch (err) {
27
- if (err.code === "ENOENT") {
28
- const defaultData = { title: "Blog", articles: [] };
29
- await save(filename, defaultData);
30
- f(defaultData.title, defaultData.articles);
31
- } else {
32
- throw err;
33
- }
47
+ await fs.writeFile(articlesFilename, "");
34
48
  }
35
49
  }
@@ -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,30 @@
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
+ }
12
+
13
+ async initialize() {
14
+ try {
15
+ this.sequelize = new Sequelize({
16
+ dialect: "sqlite",
17
+ storage: this.dbname + ".db",
18
+ logging: false,
19
+ });
20
+ await this.initializeModels();
21
+ } catch (err) {
22
+ if (err.message.includes("Please install")) {
23
+ throw new Error(
24
+ "SQLite driver is not installed. Please install it: npm install sqlite3 --save-dev"
25
+ );
26
+ }
27
+ throw err;
28
+ }
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
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"
@@ -32,6 +30,12 @@
32
30
  "dom-parser": "^1.1.5",
33
31
  "eslint": "^9.8.0",
34
32
  "eslint-plugin-jest": "^28.6.0",
35
- "jest": "^29.7.0"
33
+ "jest": "^29.7.0",
34
+ "sqlite3": "^5.1.7"
35
+ },
36
+ "optionalDependencies": {
37
+ "pg": "^8.16.3",
38
+ "pg-hstore": "^2.3.4",
39
+ "sequelize": "^6.37.7"
36
40
  }
37
41
  }
@@ -1,4 +1,4 @@
1
- body { font-family: Arial; }body { font-family: Arial, sans-serif; } h1 { color: #333; } .grid {
1
+ body { font-family: Arial; } .grid {
2
2
  border: 0 solid #000;
3
3
  display: grid;
4
4
  gap: 0.25rem;
@@ -65,4 +65,4 @@ nav a:visited {
65
65
  }
66
66
  }
67
67
 
68
- /* source-hash: a07f631befba4b6bc703f8709f5ef455faafeff4e5f00b62f835576eea7fb529 */
68
+ /* source-hash: bcb6644ec5b5c6f9685c9ad6c14ee551a6f908b8a2c372d3294e2d2e80d17fb7 */
@@ -2,7 +2,17 @@ import Blog from "../Blog.js";
2
2
  import Article from "../Article.js";
3
3
  import { fetchData, postData } from "../model/APIModel.js";
4
4
  import { server } from "./simpleServer.js";
5
- import { load as loadFromFile } from "../model/FileModel.js";
5
+ import fs from "fs";
6
+
7
+ import {
8
+ saveInfo,
9
+ loadInfo,
10
+ appendArticle,
11
+ loadArticles,
12
+ initFiles,
13
+ } from "../model/FileModel.js";
14
+
15
+ import SqliteAdapter from "../model/SqliteAdapter.js";
6
16
 
7
17
  function generateRandomContent(length) {
8
18
  let str = "";
@@ -16,21 +26,40 @@ function generateRandomContent(length) {
16
26
  }
17
27
 
18
28
  describe("File Model test", () => {
19
- it("should load blog data from blog.json", async () => {
20
- const blog = new Blog();
21
- blog.setStyle(
22
- "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
23
- );
29
+ it("should init files and load info", async () => {
30
+ const infoFile = "test_bloginfo.json";
31
+ const articlesFile = "test_articles.txt";
32
+
33
+ // Ensure clean state
34
+ if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
35
+ if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
36
+
37
+ await initFiles(infoFile, articlesFile);
38
+
39
+ const info = await loadInfo(infoFile);
40
+ expect(info.title).toBe("Blog");
24
41
 
42
+ // Cleanup
43
+ fs.unlinkSync(infoFile);
44
+ fs.unlinkSync(articlesFile);
45
+ });
46
+
47
+ it("should write to articles.txt file", async () => {
25
48
  const content = generateRandomContent(200);
49
+ const article = new Article("hello", content, new Date().toISOString());
50
+ const filename = "test_articles.txt";
26
51
 
27
- const article = new Article("hello", content);
28
- blog.addArticle(article);
52
+ await appendArticle(filename, article);
53
+ const articles = await loadArticles(filename);
29
54
 
30
- await loadFromFile("blog.json", async (title) => {
31
- expect(await blog.toHTML()).toContain(content);
32
- expect(blog).toBeDefined();
33
- });
55
+ // Compare properties since 'articles' contains plain objects, not Article instances
56
+ expect(articles.length).toBeGreaterThan(0);
57
+ const lastArticle = articles[articles.length - 1];
58
+ expect(lastArticle.title).toBe(article.title);
59
+ expect(lastArticle.content).toBe(article.content);
60
+
61
+ // Cleanup
62
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
34
63
  });
35
64
  });
36
65
 
@@ -117,11 +146,10 @@ describe("Database Model test", () => {
117
146
 
118
147
  it("should load blog data from sqlite database", async () => {
119
148
  const blog = new Blog();
120
- blog.database.type = "sqlite";
121
- blog.database.dbname = "test_" + Date.now();
122
- blog.setStyle(
123
- "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
124
- );
149
+ const sqliteAdapter = new SqliteAdapter({
150
+ dbname: "blog",
151
+ });
152
+ blog.setDatabaseAdapter(sqliteAdapter);
125
153
  await blog.init();
126
154
 
127
155
  const content = generateRandomContent(200);