@lexho111/plainblog 0.0.10 → 0.0.11

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
@@ -1,10 +1,13 @@
1
1
  import http from "http";
2
2
  import fetch from "node-fetch";
3
3
  import { URLSearchParams } from "url";
4
+ import { fromEvent, firstValueFrom } from "rxjs";
5
+ import { map, scan, takeUntil, last, defaultIfEmpty } from "rxjs/operators";
4
6
  import Article from "./Article.js";
5
7
  import DatabaseModel from "./model/DatabaseModel.js";
6
8
  import { fetchData, postData } from "./model/APIModel.js";
7
9
  import { save as saveToFile, load as loadFromFile } from "./model/fileModel.js";
10
+ import { formatHTML, formatMarkdown, validate } from "./Formatter.js";
8
11
 
9
12
  export default class Blog {
10
13
  constructor() {
@@ -16,8 +19,8 @@ export default class Blog {
16
19
 
17
20
  this.database = {
18
21
  type: "sqlite",
19
- username: "user",
20
- password: "password",
22
+ username: "user1",
23
+ password: "password1",
21
24
  host: "localhost",
22
25
  dbname: "blog",
23
26
  };
@@ -44,53 +47,68 @@ export default class Blog {
44
47
  /** initializes database */
45
48
  async init() {
46
49
  if (this.#isExternalAPI) {
50
+ console.log("external API");
47
51
  await this.loadFromAPI();
48
52
  } else {
49
- console.log(`initialize ${this.database.type} database`);
53
+ console.log(`database: ${this.database.type}`);
50
54
  this.#databaseModel = new DatabaseModel(this.database);
55
+ console.log(`connected to database`);
51
56
  await this.#databaseModel.initialize();
52
57
  const dbTitle = await this.#databaseModel.getBlogTitle();
53
58
  const dbArticles = await this.#databaseModel.findAll();
54
- const responseData = { title: dbTitle, articles: dbArticles };
59
+ let title = "";
60
+ if (this.title.length > 0) title = this.title; // use blog title if set
61
+ else title = dbTitle;
62
+ const responseData = { title: title, articles: dbArticles };
55
63
  this.#applyBlogData(responseData);
56
64
  }
57
65
  }
58
66
 
59
67
  /** post a blog article */
60
68
  async postArticle(req, res) {
61
- let body = "";
69
+ // OLD CODE
70
+ /*let body = "";
62
71
  req.on("data", (chunk) => {
63
72
  body += chunk.toString();
64
- });
65
-
66
- req.on("end", async () => {
67
- const params = new URLSearchParams(body);
68
- const title = params.get("title");
69
- const content = params.get("content");
70
-
71
- if (title && content) {
72
- const newArticleData = { title, content };
73
- try {
74
- // Save the new article to the database via the ApiServer
75
- if (this.#databaseModel)
76
- await this.#databaseModel.save(newArticleData);
77
- if (this.#isExternalAPI) postData(this.#apiUrl, newArticleData);
78
- // Add the article to the local list for immediate display
79
- this.addArticle(new Article(title, content));
80
- } catch (error) {
81
- console.error("Failed to post new article to API:", error);
82
- }
73
+ });*/
74
+ // NEW CODE
75
+ // Create a stream of the body string
76
+ const body$ = fromEvent(req, "data").pipe(
77
+ map((chunk) => chunk.toString()),
78
+ scan((acc, chunk) => acc + chunk, ""), // Accumulate chunks
79
+ takeUntil(fromEvent(req, "end")), // Stop when 'end' emits
80
+ last(), // Emit only the final full string
81
+ defaultIfEmpty("") // Handle empty bodies
82
+ );
83
+
84
+ const body = await firstValueFrom(body$);
85
+
86
+ //req.on("end", async () => {
87
+ const params = new URLSearchParams(body);
88
+ const title = params.get("title");
89
+ const content = params.get("content");
90
+
91
+ if (title && content) {
92
+ const newArticleData = { title, content };
93
+ try {
94
+ // Save the new article to the database via the ApiServer
95
+ if (this.#databaseModel) await this.#databaseModel.save(newArticleData);
96
+ if (this.#isExternalAPI) await postData(this.#apiUrl, newArticleData);
97
+ // Add the article to the local list for immediate display
98
+ this.addArticle(new Article(title, content));
99
+ } catch (error) {
100
+ console.error("Failed to post new article to API:", error);
83
101
  }
102
+ }
84
103
 
85
- res.writeHead(303, { Location: "/" });
86
- res.end();
87
- });
104
+ res.writeHead(303, { Location: "/" });
105
+ res.end();
106
+ //});
88
107
  }
89
108
 
90
109
  /** start a http server with default port 8080 */
91
110
  async startServer(port = 8080) {
92
111
  if (this.#databaseModel === undefined) await this.init(); // init blog if it didn't already happen
93
- await this.#databaseModel.initialize();
94
112
 
95
113
  const server = http.createServer(async (req, res) => {
96
114
  // API routes
@@ -122,7 +140,7 @@ export default class Blog {
122
140
 
123
141
  return new Promise((resolve) => {
124
142
  this.#server.listen(port, () => {
125
- console.log(`Server running at http://localhost:${port}/`);
143
+ console.log(`server running at http://localhost:${port}/`);
126
144
  resolve(); // Resolve the promise when the server is listening
127
145
  });
128
146
  });
@@ -219,67 +237,12 @@ export default class Blog {
219
237
 
220
238
  /** print markdown to the console */
221
239
  async print() {
222
- console.log(`# ${this.title}`);
223
- for (const article of this.articles) {
224
- console.log(`## ${article.title}`);
225
- console.log(article.content);
226
- }
227
- }
228
-
229
- /** format this blog content to html */
230
- formatter(data) {
231
- return `<!DOCTYPE html>
232
- <html lang="de">
233
- <head>
234
- <meta charset="UTF-8">
235
- <title>${data.title}</title>
236
- <style>${data.style}</style>
237
- </head>
238
- <body>
239
- <h1>${data.title}</h1>
240
- <div style="width: 500px;">
241
- <form action="/" method="POST">
242
- <h3>Add a New Article</h3>
243
- <input type="text" name="title" placeholder="Article Title" required style="display: block; width: 300px; margin-bottom: 10px;">
244
- <textarea name="content" placeholder="Article Content" required style="display: block; width: 300px; height: 100px; margin-bottom: 10px;"></textarea>
245
- <button type="submit">Add Article</button>
246
- </form>
247
- <hr>
248
- ${data.articles
249
- .map(
250
- (article) => `
251
- <article>
252
- <h2>${article.title}</h2>
253
- <p>${article.getContentShort()}</p>
254
- </article>`
255
- )
256
- .join("")}
257
- </div>
258
- </body>
259
- </html>`;
260
- }
261
-
262
- validate(html) {
263
- let test = true; // all tests passed
264
- if (!(html.includes("<html") && html.includes("</html"))) {
265
- console.error("html not ok");
266
- test = false;
267
- }
268
- if (!(html.includes("<head") && html.includes("</head"))) {
269
- console.error("head not ok");
270
- test = false;
271
- }
272
- if (!(html.includes("<body") && html.includes("</body"))) {
273
- console.error("body not ok");
274
- test = false;
275
- }
276
- return test;
277
- }
278
-
279
- toHTMLFunc(data, formatter, validate) {
280
- const html = formatter(data);
281
- if (validate(html)) return html;
282
- else throw new Error("Error. Invalid HTML!");
240
+ const data = {
241
+ title: this.title,
242
+ articles: this.articles,
243
+ };
244
+ const markdown = formatMarkdown(data);
245
+ console.log(markdown);
283
246
  }
284
247
 
285
248
  /** render this blog content to valid html */
@@ -289,6 +252,8 @@ export default class Blog {
289
252
  style: this.style,
290
253
  articles: this.articles,
291
254
  };
292
- return this.toHTMLFunc(data, this.formatter, this.validate);
255
+ const html = formatHTML(data);
256
+ if (validate(html)) return html;
257
+ else throw new Error("Error. Invalid HTML!");
293
258
  }
294
259
  }
package/Formatter.js ADDED
@@ -0,0 +1,80 @@
1
+ /** format content to html */
2
+ export function formatHTML(data) {
3
+ const script = `function generateRandomContent(length) {
4
+ let str = "";
5
+ const char = "A";
6
+ for (let i = 0; i < length; i++) {
7
+ const rnd = Math.random() * ("z".charCodeAt(0) - "A".charCodeAt(0)); // A z.charCodeAt(0)"
8
+ const char1 = String.fromCharCode(char.charCodeAt(0) + rnd);
9
+ str += char1;
10
+ }
11
+ return str;
12
+ }
13
+ function fillWithContent() {
14
+ let title = document.getElementById("title");
15
+ let cont = document.getElementById("content");
16
+ cont.value = generateRandomContent(200);
17
+ title.value = generateRandomContent(50);
18
+ }`;
19
+ const button = `<button type="button" onClick="fillWithContent();" style="margin: 4px;">generate random text</button>`;
20
+ return `<!DOCTYPE html>
21
+ <html lang="de">
22
+ <head>
23
+ <meta charset="UTF-8">
24
+ <title>${data.title}</title>
25
+ <style>${data.style}</style>
26
+ </head>
27
+ <body>
28
+ <h1>${data.title}</h1>
29
+ <div style="width: 500px;">
30
+ <form action="/" method="POST">
31
+ <h3>Add a New Article</h3>
32
+ <input type="text" id="title" name="title" placeholder="Article Title" required style="display: block; width: 300px; margin-bottom: 10px;">
33
+ <textarea id="content" name="content" placeholder="Article Content" required style="display: block; width: 300px; height: 100px; margin-bottom: 10px;"></textarea>
34
+ <button type="submit">Add Article</button>
35
+ <script>
36
+ ${script}
37
+ </script>
38
+ </form>
39
+ <hr>
40
+ ${data.articles
41
+ .map(
42
+ (article) => `
43
+ <article style="overflow-wrap: break-word;">
44
+ <h2>${article.title}</h2>
45
+ <p>${article.getContentShort()}</p>
46
+ </article>`
47
+ )
48
+ .join("")}
49
+ </div>
50
+ </body>
51
+ </html>`;
52
+ }
53
+
54
+ export function formatMarkdown(data) {
55
+ let markdown = "";
56
+ markdown += `# ${data.title}\n`;
57
+ for (const article of data.articles) {
58
+ markdown += `## ${article.title}\n`;
59
+ markdown += article.content;
60
+ markdown += "\n";
61
+ }
62
+ return markdown;
63
+ }
64
+
65
+ export function validate(html) {
66
+ let test = true; // all tests passed
67
+ if (!(html.includes("<html") && html.includes("</html"))) {
68
+ console.error("html not ok");
69
+ test = false;
70
+ }
71
+ if (!(html.includes("<head") && html.includes("</head"))) {
72
+ console.error("head not ok");
73
+ test = false;
74
+ }
75
+ if (!(html.includes("<body") && html.includes("</body"))) {
76
+ console.error("body not ok");
77
+ test = false;
78
+ }
79
+ return test;
80
+ }
package/README.md CHANGED
@@ -42,10 +42,17 @@ await blog.init(); // load data from database
42
42
  blog.startServer(8080);
43
43
  ```
44
44
 
45
- ### set an external API to fetch data from an external database
45
+ ### set an API to fetch data from an external database
46
46
 
47
47
  ```
48
+ import Blog from "@lexho111/plainblog";
49
+
50
+ const blog = new Blog();
48
51
  blog.setAPI("http://example.com:5432/blog")
52
+ blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
53
+ await blog.init(); // load data from database
54
+
55
+ blog.startServer(8080);
49
56
  ```
50
57
 
51
58
  save data to file
package/blog.db CHANGED
Binary file
package/model/APIModel.js CHANGED
@@ -1,20 +1,31 @@
1
+ import fetch from "node-fetch";
2
+ import { defer, firstValueFrom, timer, of } from "rxjs";
3
+ import { retry, mergeMap, catchError } from "rxjs/operators";
4
+
1
5
  // EXTERNAL DATA API
2
6
 
3
7
  /** fetch data from an URL */
4
8
  export async function fetchData(apiUrl) {
5
- try {
6
- const response = await fetch(apiUrl);
7
-
8
- if (!response.ok) {
9
- throw new Error(`HTTP error! status: ${response.status}`);
10
- }
9
+ const request$ = defer(() => fetch(apiUrl)).pipe(
10
+ // 1. Check response status inside the stream
11
+ mergeMap(async (response) => {
12
+ if (!response.ok)
13
+ throw new Error(`HTTP error! status: ${response.status}`);
14
+ return response.json();
15
+ }),
16
+ // 2. Retry 3 times, waiting 1s, 2s, 3s respectively
17
+ retry({
18
+ count: 3,
19
+ delay: (error, retryCount) => timer(retryCount * 1000),
20
+ }),
21
+ // 3. Handle final failure
22
+ catchError((error) => {
23
+ console.error("Failed to fetch data after retries:", error);
24
+ return of(null);
25
+ })
26
+ );
11
27
 
12
- const data = await response.json();
13
- return data;
14
- } catch (error) {
15
- console.error("Failed to fetch data:", error);
16
- return null;
17
- }
28
+ return firstValueFrom(request$);
18
29
  }
19
30
 
20
31
  /** post data to an URL */
@@ -49,7 +49,7 @@ export default class DatabaseModel {
49
49
  "Article",
50
50
  {
51
51
  title: DataTypes.STRING,
52
- content: DataTypes.STRING,
52
+ content: DataTypes.TEXT,
53
53
  },
54
54
  {
55
55
  timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
@@ -70,12 +70,13 @@ export default class DatabaseModel {
70
70
  async initialize() {
71
71
  // This creates the tables if they don't exist.
72
72
  await this.#sequelize.sync({ alter: true });
73
+ console.log("database tables synced and ready.");
73
74
 
74
75
  // Check for and create the initial blog title right after syncing.
75
76
  const blogInfoCount = await this.#BlogInfo.count();
76
77
  if (blogInfoCount === 0) {
77
78
  await this.#BlogInfo.create({ title: "My Default Blog Title" });
78
- console.log("Initialized blog title in database.");
79
+ console.log("initialized blog title in database.");
79
80
  }
80
81
  }
81
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,6 +19,7 @@
19
19
  "node-fetch": "^3.3.2",
20
20
  "pg": "^8.16.3",
21
21
  "pg-hstore": "^2.3.4",
22
+ "rxjs": "^7.8.2",
22
23
  "sequelize": "^6.37.7",
23
24
  "sqlite3": "^5.1.7"
24
25
  },
package/test/blog.test.js CHANGED
@@ -61,7 +61,7 @@ describe("test blog", () => {
61
61
  expect(html).toContain(style);
62
62
  }
63
63
  });
64
- test("print method logs title and articles to console", () => {
64
+ test("print method logs title and articles to console", async () => {
65
65
  // Arrange: Set up the blog with a title and articles
66
66
  const myblog = new Blog();
67
67
  myblog.setTitle("My Test Blog");
@@ -71,12 +71,14 @@ describe("test blog", () => {
71
71
  const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {});
72
72
 
73
73
  // Act: Call the method we want to test
74
- myblog.print();
74
+ await myblog.print();
75
75
 
76
76
  // Assert: Check if console.log was called with the correct strings
77
- expect(consoleSpy).toHaveBeenCalledWith("# My Test Blog");
78
- expect(consoleSpy).toHaveBeenCalledWith("## Article 1");
79
- expect(consoleSpy).toHaveBeenCalledWith("Content 1");
77
+ expect(consoleSpy).toHaveBeenCalled();
78
+ const output = consoleSpy.mock.calls[0][0];
79
+ expect(output).toContain("# My Test Blog");
80
+ expect(output).toContain("## Article 1");
81
+ expect(output).toContain("Content 1");
80
82
 
81
83
  // Clean up the spy to restore the original console.log
82
84
  consoleSpy.mockRestore();
@@ -4,10 +4,10 @@ import { fetchData, postData } from "../model/APIModel.js";
4
4
  import { server } from "./simpleServer.js";
5
5
 
6
6
  function generateRandomContent(length) {
7
- const char = "a";
8
7
  let str = "";
8
+ const char = "A";
9
9
  for (let i = 0; i < length; i++) {
10
- const rnd = Math.random() * 10;
10
+ const rnd = Math.random() * ("z".charCodeAt(0) - "A".charCodeAt(0)); // A z.charCodeAt(0)"
11
11
  const char1 = String.fromCharCode(char.charCodeAt(0) + rnd);
12
12
  str += char1;
13
13
  }
@@ -59,9 +59,14 @@ describe("API Model test", () => {
59
59
  await new Promise((resolve) => {
60
60
  const req = {
61
61
  on: (event, cb) => {
62
- if (event === "data") cb(`title=hello&content=${content}`);
63
- if (event === "end") cb();
62
+ if (event === "data") {
63
+ setTimeout(() => cb(`title=hello&content=${content}`), 0);
64
+ }
65
+ if (event === "end") {
66
+ setTimeout(() => cb(), 10);
67
+ }
64
68
  },
69
+ off: () => {},
65
70
  };
66
71
  const res = {
67
72
  writeHead: () => {},
@@ -111,7 +116,7 @@ describe("Database Model test", () => {
111
116
  it("should load blog data from sqlite database", async () => {
112
117
  const blog = new Blog();
113
118
  blog.database.type = "sqlite";
114
- blog.database.dbname = "test";
119
+ blog.database.dbname = "test_" + Date.now();
115
120
  blog.setStyle(
116
121
  "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
117
122
  );
@@ -122,9 +127,14 @@ describe("Database Model test", () => {
122
127
  await new Promise((resolve) => {
123
128
  const req = {
124
129
  on: (event, cb) => {
125
- if (event === "data") cb(`title=hello&content=${content}`);
126
- if (event === "end") cb();
130
+ if (event === "data") {
131
+ setTimeout(() => cb(`title=hello&content=${content}`), 0);
132
+ }
133
+ if (event === "end") {
134
+ setTimeout(() => cb(), 20);
135
+ }
127
136
  },
137
+ off: () => {},
128
138
  };
129
139
  const res = {
130
140
  writeHead: () => {},
@@ -13,6 +13,7 @@ import Blog from "../Blog.js";
13
13
 
14
14
  const PORT = 8080;
15
15
  const apiBaseUrl = `http://localhost:${PORT}`;
16
+
16
17
  describe("server test", () => {
17
18
  const blog = new Blog(); // Create one blog instance for all tests
18
19
  blog.setTitle("My Blog");
@@ -23,6 +24,7 @@ describe("server test", () => {
23
24
  blog.setStyle(
24
25
  "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
25
26
  );
27
+ await blog.init();
26
28
  // Await it to ensure the server is running before tests start.
27
29
  await blog.startServer(PORT);
28
30
  });
@@ -33,12 +35,35 @@ describe("server test", () => {
33
35
  await blog.closeServer();
34
36
  });
35
37
 
36
- test("should post a new article via /api/articles endpoint", async () => {
38
+ test("check if server responds to /", async () => {
39
+ // get articles from blog api
40
+ const response = await fetch(`${apiBaseUrl}/`, {
41
+ method: "GET",
42
+ });
43
+
44
+ expect(response.status).toBe(200);
45
+ const html = await response.text();
46
+ expect(html).toContain("My Blog");
47
+ });
48
+
49
+ test("check if server responds to /api", async () => {
50
+ // get articles from blog api
51
+ const response = await fetch(`${apiBaseUrl}/api`, {
52
+ method: "GET",
53
+ });
54
+
55
+ expect(response.status).toBe(200);
56
+ const responseData = await response.json();
57
+ expect(responseData.title).toBe("My Blog");
58
+ });
59
+
60
+ test("should post a new article via /api/articles", async () => {
37
61
  const newArticle = {
38
62
  title: "Test Title from Jest",
39
63
  content: "This is the content of the test article.",
40
64
  };
41
65
 
66
+ // post new article to the blog
42
67
  const response = await fetch(`${apiBaseUrl}/api/articles`, {
43
68
  method: "POST",
44
69
  headers: { "Content-Type": "application/json" },
@@ -48,5 +73,7 @@ describe("server test", () => {
48
73
  expect(response.status).toBe(201);
49
74
  const responseData = await response.json();
50
75
  expect(responseData).toEqual(newArticle);
76
+
77
+ expect(blog.toHTML()).toContain(newArticle.content); // does blog contain my new article?
51
78
  });
52
79
  });