@lexho111/plainblog 0.5.8 → 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/README.md +1 -1
- package/articles.txt +3 -0
- package/blog.db +0 -0
- package/bloginfo.json +3 -0
- package/model/FileAdapter.js +23 -39
- package/model/FileModel.js +36 -22
- package/model/SqliteAdapter.js +15 -6
- package/package.json +3 -2
- package/test/model.test.js +45 -17
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. Your data will be stored by default in
|
|
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
|
|
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
package/model/FileAdapter.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
saveInfo,
|
|
3
|
+
loadInfo,
|
|
4
|
+
appendArticle,
|
|
5
|
+
loadArticles,
|
|
6
|
+
initFiles,
|
|
7
|
+
} from "./FileModel.js";
|
|
2
8
|
|
|
3
9
|
export default class FileAdapter {
|
|
4
10
|
dbtype = "file";
|
|
5
11
|
constructor(options) {
|
|
6
|
-
this.
|
|
12
|
+
this.infoFile = "bloginfo.json";
|
|
13
|
+
this.articlesFile = "articles.txt";
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
async initialize() {
|
|
10
17
|
console.log("file adapter init");
|
|
18
|
+
await initFiles(this.infoFile, this.articlesFile);
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
test() {
|
|
@@ -15,42 +23,19 @@ export default class FileAdapter {
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
async getBlogTitle() {
|
|
18
|
-
const
|
|
19
|
-
return
|
|
20
|
-
res(blogTitle);
|
|
21
|
-
});
|
|
26
|
+
const info = await loadInfo(this.infoFile);
|
|
27
|
+
return info.title || "Blog";
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
async save(newArticle) {
|
|
25
31
|
if (!newArticle.createdAt) {
|
|
26
32
|
newArticle.createdAt = new Date().toISOString();
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
let blogTitle = "";
|
|
30
|
-
let articles = [];
|
|
31
|
-
try {
|
|
32
|
-
await loadFromFile(this.filename, (t, a) => {
|
|
33
|
-
blogTitle = t;
|
|
34
|
-
articles = a || [];
|
|
35
|
-
});
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.error(err);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
articles.push(newArticle);
|
|
41
|
-
saveToFile(this.filename, { title: blogTitle, articles });
|
|
34
|
+
await appendArticle(this.articlesFile, newArticle);
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
async updateBlogTitle(newTitle) {
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await loadFromFile(this.filename, (t, a) => {
|
|
48
|
-
articles = a || [];
|
|
49
|
-
});
|
|
50
|
-
} catch (err) {
|
|
51
|
-
console.error(err);
|
|
52
|
-
}
|
|
53
|
-
saveToFile(this.filename, { title: newTitle, articles });
|
|
38
|
+
await saveInfo(this.infoFile, { title: newTitle });
|
|
54
39
|
}
|
|
55
40
|
|
|
56
41
|
async findAll(
|
|
@@ -62,16 +47,15 @@ export default class FileAdapter {
|
|
|
62
47
|
) {
|
|
63
48
|
let dbArticles = [];
|
|
64
49
|
try {
|
|
65
|
-
await
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
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
|
+
}
|
|
75
59
|
} catch (err) {
|
|
76
60
|
console.error(err);
|
|
77
61
|
}
|
package/model/FileModel.js
CHANGED
|
@@ -1,35 +1,49 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
|
|
3
|
-
/** save blog
|
|
4
|
-
export async function
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
12
|
-
|
|
11
|
+
const data = await fs.readFile(filename, "utf8");
|
|
12
|
+
return JSON.parse(data);
|
|
13
13
|
} catch (err) {
|
|
14
|
-
|
|
14
|
+
return { title: "Blog" };
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
export async function
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
}
|
package/model/SqliteAdapter.js
CHANGED
|
@@ -8,14 +8,23 @@ export default class SqliteAdapter extends SequelizeAdapter {
|
|
|
8
8
|
|
|
9
9
|
// Use the full path for the database file from the options.
|
|
10
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
11
|
}
|
|
17
12
|
|
|
18
13
|
async initialize() {
|
|
19
|
-
|
|
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
|
+
}
|
|
20
29
|
}
|
|
21
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.5.
|
|
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",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dom-parser": "^1.1.5",
|
|
31
31
|
"eslint": "^9.8.0",
|
|
32
32
|
"eslint-plugin-jest": "^28.6.0",
|
|
33
|
-
"jest": "^29.7.0"
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"sqlite3": "^5.1.7"
|
|
34
35
|
},
|
|
35
36
|
"optionalDependencies": {
|
|
36
37
|
"pg": "^8.16.3",
|
package/test/model.test.js
CHANGED
|
@@ -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
|
|
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
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
52
|
+
await appendArticle(filename, article);
|
|
53
|
+
const articles = await loadArticles(filename);
|
|
29
54
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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);
|