@lexho111/plainblog 0.0.9 → 0.0.10

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,9 +1,10 @@
1
1
  import http from "http";
2
2
  import fetch from "node-fetch";
3
- import { promises as fs } from "fs";
4
3
  import { URLSearchParams } from "url";
5
4
  import Article from "./Article.js";
6
- import ApiServer from "./model.js";
5
+ import DatabaseModel from "./model/DatabaseModel.js";
6
+ import { fetchData, postData } from "./model/APIModel.js";
7
+ import { save as saveToFile, load as loadFromFile } from "./model/fileModel.js";
7
8
 
8
9
  export default class Blog {
9
10
  constructor() {
@@ -24,9 +25,9 @@ export default class Blog {
24
25
 
25
26
  // Private fields
26
27
  #server = null;
27
- #apiServer;
28
+ #databaseModel;
28
29
  #isExternalAPI = false;
29
- #apiDataLoaded = false;
30
+ #apiUrl = "";
30
31
 
31
32
  setTitle(title) {
32
33
  this.title = title;
@@ -43,56 +44,20 @@ export default class Blog {
43
44
  /** initializes database */
44
45
  async init() {
45
46
  if (this.#isExternalAPI) {
46
- await this.loadFromAPI(this.#APIUrl);
47
+ await this.loadFromAPI();
47
48
  } else {
48
49
  console.log(`initialize ${this.database.type} database`);
49
- this.#apiServer = new ApiServer(this.database);
50
- const dbTitle = await this.#apiServer.getBlogTitle();
51
- const dbArticles = await this.#apiServer.findAll();
50
+ this.#databaseModel = new DatabaseModel(this.database);
51
+ await this.#databaseModel.initialize();
52
+ const dbTitle = await this.#databaseModel.getBlogTitle();
53
+ const dbArticles = await this.#databaseModel.findAll();
52
54
  const responseData = { title: dbTitle, articles: dbArticles };
53
- this.#loadFromData(responseData);
55
+ this.#applyBlogData(responseData);
54
56
  }
55
57
  }
56
58
 
57
- // EXTERNAL DATA API
58
- #APIUrl = "";
59
-
60
- setAPI(APIUrl) {
61
- this.#APIUrl = APIUrl;
62
- this.#isExternalAPI = true;
63
- }
64
-
65
- /** fetch data from an URL */
66
- async #fetchDataFromApi(apiUrl) {
67
- try {
68
- const response = await fetch(apiUrl);
69
-
70
- if (!response.ok) {
71
- throw new Error(`HTTP error! status: ${response.status}`);
72
- }
73
-
74
- const data = await response.json();
75
- return data;
76
- } catch (error) {
77
- console.error("Failed to fetch data:", error);
78
- return null;
79
- }
80
- }
81
-
82
- /** post data to an URL */
83
- async #postDataToApi(apiUrl, postData) {
84
- const response = await fetch(apiUrl, {
85
- method: "POST",
86
- body: JSON.stringify(postData),
87
- headers: { "Content-Type": "application/json" },
88
- });
89
-
90
- const data = await response.json();
91
- return data;
92
- }
93
-
94
59
  /** post a blog article */
95
- async #postArticle(req, res) {
60
+ async postArticle(req, res) {
96
61
  let body = "";
97
62
  req.on("data", (chunk) => {
98
63
  body += chunk.toString();
@@ -107,7 +72,9 @@ export default class Blog {
107
72
  const newArticleData = { title, content };
108
73
  try {
109
74
  // Save the new article to the database via the ApiServer
110
- await this.#apiServer.save(newArticleData);
75
+ if (this.#databaseModel)
76
+ await this.#databaseModel.save(newArticleData);
77
+ if (this.#isExternalAPI) postData(this.#apiUrl, newArticleData);
111
78
  // Add the article to the local list for immediate display
112
79
  this.addArticle(new Article(title, content));
113
80
  } catch (error) {
@@ -120,42 +87,10 @@ export default class Blog {
120
87
  });
121
88
  }
122
89
 
123
- /** everything that happens in /api */
124
- async #jsonAPI(req, res) {
125
- // GET all blog data
126
- if (req.method === "GET" && req.url === "/api") {
127
- // controller
128
- res.writeHead(200, { "Content-Type": "application/json" });
129
- const dbArticles = await this.#apiServer.findAll();
130
- const responseData = {
131
- title: this.title, // Keep the title from the original constant
132
- articles: dbArticles,
133
- };
134
- console.log(`responseData: ${responseData}`);
135
- res.end(JSON.stringify(responseData));
136
- }
137
-
138
- // POST a new article
139
- else if (req.method === "POST" && req.url === "/api/articles") {
140
- let body = "";
141
- req.on("data", (chunk) => (body += chunk.toString()));
142
- req.on("end", async () => {
143
- const newArticle = JSON.parse(body);
144
-
145
- // local
146
- await this.#apiServer.save(newArticle); // --> to api server
147
- // extern
148
-
149
- res.writeHead(201, { "Content-Type": "application/json" });
150
- res.end(JSON.stringify(newArticle));
151
- });
152
- }
153
- }
154
-
155
90
  /** start a http server with default port 8080 */
156
91
  async startServer(port = 8080) {
157
- if (this.#apiServer === undefined) this.init(); // init blog if it didn't already happen
158
- await this.#apiServer.initialize();
92
+ if (this.#databaseModel === undefined) await this.init(); // init blog if it didn't already happen
93
+ await this.#databaseModel.initialize();
159
94
 
160
95
  const server = http.createServer(async (req, res) => {
161
96
  // API routes
@@ -167,8 +102,8 @@ export default class Blog {
167
102
  // Web Page Routes
168
103
  // POST new article
169
104
  if (req.method === "POST" && req.url === "/") {
170
- await this.#apiServer.updateBlogTitle(this.title);
171
- await this.#postArticle(req, res);
105
+ await this.#databaseModel.updateBlogTitle(this.title);
106
+ await this.postArticle(req, res);
172
107
  // GET artciles
173
108
  } else if (req.method === "GET" && req.url === "/") {
174
109
  // load articles
@@ -207,42 +142,8 @@ export default class Blog {
207
142
  });
208
143
  }
209
144
 
210
- /** save blog content to file */
211
- async save(filename = this.filename) {
212
- if (this.#apiServer === undefined) this.init(); // init blog if it didn't already happen
213
- //await this.#apiServer.initialize();
214
- if (this.#isExternalAPI) await this.loadFromAPI(this.#APIUrl);
215
- if (!filename) {
216
- console.error("Error: Filename not provided and not set previously.");
217
- return;
218
- }
219
- this.filename = filename;
220
- const data = {
221
- title: this.title,
222
- articles: this.articles,
223
- };
224
-
225
- try {
226
- await fs.writeFile(filename, JSON.stringify(data, null, 2));
227
- console.log(`Blog data saved to ${filename}`);
228
- } catch (err) {
229
- console.error("Error saving blog data:", err);
230
- }
231
- }
232
-
233
- /** load blog content from file */
234
- async load(filename) {
235
- this.filename = filename;
236
- const data = await fs.readFile(filename, "utf8");
237
- const jsonData = JSON.parse(data);
238
- this.title = jsonData.title;
239
- this.articles = jsonData.articles.map(
240
- (article) => new Article(article.title, article.content)
241
- );
242
- }
243
-
244
145
  /** Populates the blog's title and articles from a data object. */
245
- #loadFromData(data) {
146
+ #applyBlogData(data) {
246
147
  this.articles = []; // Clear existing articles before loading new ones
247
148
  this.setTitle(data.title);
248
149
  // Assuming the API returns an array of objects with title and content
@@ -250,17 +151,72 @@ export default class Blog {
250
151
  for (const articleData of data.articles) {
251
152
  this.addArticle(new Article(articleData.title, articleData.content));
252
153
  }
253
- this.#apiDataLoaded = true; // Mark that we have successfully loaded data
254
154
  }
255
155
  }
256
156
 
257
- async loadFromAPI(apiUrl) {
258
- const data = await this.#fetchDataFromApi(apiUrl);
157
+ async save(filename = this.filename) {
158
+ if (this.#databaseModel === undefined) this.init(); // init blog if it didn't already happen
159
+ //await this.#apiServer.initialize();
160
+ if (this.#isExternalAPI) await this.loadFromAPI();
161
+ const blogData = { title: this.title, articles: this.articles };
162
+ saveToFile(filename, blogData);
163
+ }
164
+
165
+ async load(filename) {
166
+ loadFromFile(filename, (title, articles) => {
167
+ this.title = title;
168
+ this.articles = articles.map(
169
+ (article) => new Article(article.title, article.content)
170
+ );
171
+ });
172
+ }
173
+
174
+ async loadFromAPI() {
175
+ const data = await fetchData(this.#apiUrl);
259
176
  if (data) {
260
- this.#loadFromData(data);
177
+ this.#applyBlogData(data);
178
+ }
179
+ }
180
+
181
+ // controller
182
+ /** everything that happens in /api */
183
+ async #jsonAPI(req, res) {
184
+ // GET all blog data
185
+ if (req.method === "GET" && req.url === "/api") {
186
+ // controller
187
+ res.writeHead(200, { "Content-Type": "application/json" });
188
+ const dbArticles = await this.#databaseModel.findAll();
189
+ const responseData = {
190
+ title: this.title, // Keep the title from the original constant
191
+ articles: dbArticles,
192
+ };
193
+ console.log(`responseData: ${responseData}`);
194
+ res.end(JSON.stringify(responseData));
195
+ }
196
+
197
+ // POST a new article
198
+ else if (req.method === "POST" && req.url === "/api/articles") {
199
+ let body = "";
200
+ req.on("data", (chunk) => (body += chunk.toString()));
201
+ req.on("end", async () => {
202
+ const newArticle = JSON.parse(body);
203
+
204
+ // local
205
+ await this.#databaseModel.save(newArticle); // --> to api server
206
+ // extern
207
+
208
+ res.writeHead(201, { "Content-Type": "application/json" });
209
+ res.end(JSON.stringify(newArticle));
210
+ });
261
211
  }
262
212
  }
263
213
 
214
+ /** set external API */
215
+ setAPI(APIUrl) {
216
+ this.#apiUrl = APIUrl;
217
+ this.#isExternalAPI = true;
218
+ }
219
+
264
220
  /** print markdown to the console */
265
221
  async print() {
266
222
  console.log(`# ${this.title}`);
@@ -270,22 +226,17 @@ export default class Blog {
270
226
  }
271
227
  }
272
228
 
273
- /** render this blog content to html */
274
- async toHTML() {
275
- // If we have an API URL and haven't loaded data yet, load it now.
276
- if (this.#APIUrl && !this.#apiDataLoaded) {
277
- await this.loadFromAPI(this.#APIUrl);
278
- }
279
-
229
+ /** format this blog content to html */
230
+ formatter(data) {
280
231
  return `<!DOCTYPE html>
281
232
  <html lang="de">
282
233
  <head>
283
234
  <meta charset="UTF-8">
284
- <title>${this.title}</title>
285
- <style>${this.style}</style>
235
+ <title>${data.title}</title>
236
+ <style>${data.style}</style>
286
237
  </head>
287
238
  <body>
288
- <h1>${this.title}</h1>
239
+ <h1>${data.title}</h1>
289
240
  <div style="width: 500px;">
290
241
  <form action="/" method="POST">
291
242
  <h3>Add a New Article</h3>
@@ -294,7 +245,7 @@ export default class Blog {
294
245
  <button type="submit">Add Article</button>
295
246
  </form>
296
247
  <hr>
297
- ${this.articles
248
+ ${data.articles
298
249
  .map(
299
250
  (article) => `
300
251
  <article>
@@ -307,4 +258,37 @@ export default class Blog {
307
258
  </body>
308
259
  </html>`;
309
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!");
283
+ }
284
+
285
+ /** render this blog content to valid html */
286
+ toHTML() {
287
+ const data = {
288
+ title: this.title,
289
+ style: this.style,
290
+ articles: this.articles,
291
+ };
292
+ return this.toHTMLFunc(data, this.formatter, this.validate);
293
+ }
310
294
  }
package/blog.db CHANGED
Binary file
package/blog.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "title": "",
3
+ "articles": [
4
+ {
5
+ "title": "hello",
6
+ "content": "hello world!"
7
+ }
8
+ ]
9
+ }
@@ -0,0 +1,30 @@
1
+ // EXTERNAL DATA API
2
+
3
+ /** fetch data from an URL */
4
+ 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
+ }
11
+
12
+ const data = await response.json();
13
+ return data;
14
+ } catch (error) {
15
+ console.error("Failed to fetch data:", error);
16
+ return null;
17
+ }
18
+ }
19
+
20
+ /** post data to an URL */
21
+ export async function postData(apiUrl, postData) {
22
+ const response = await fetch(apiUrl, {
23
+ method: "POST",
24
+ body: JSON.stringify(postData),
25
+ headers: { "Content-Type": "application/json" },
26
+ });
27
+
28
+ const data = await response.json();
29
+ return data;
30
+ }
@@ -1,6 +1,6 @@
1
1
  import { Sequelize, DataTypes } from "sequelize";
2
2
 
3
- export default class ApiServer {
3
+ export default class DatabaseModel {
4
4
  #username;
5
5
  #password;
6
6
  #host;
@@ -16,7 +16,7 @@ export default class ApiServer {
16
16
  if (databasetype === "sqlite") {
17
17
  this.#sequelize = new Sequelize({
18
18
  dialect: "sqlite",
19
- storage: "./blog.db",
19
+ storage: `./${this.#dbname}.db`,
20
20
  logging: false,
21
21
  });
22
22
  } else if (databasetype === "postgres") {
@@ -0,0 +1,25 @@
1
+ import { promises as fs } from "fs";
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
+ }
9
+
10
+ try {
11
+ await fs.writeFile(filename, JSON.stringify(data, null, 2));
12
+ console.log(`Blog data saved to ${filename}`);
13
+ } catch (err) {
14
+ console.error("Error saving blog data:", err);
15
+ }
16
+ }
17
+
18
+ /** load blog content from file */
19
+ export async function load(filename, f) {
20
+ const data = await fs.readFile(filename, "utf8");
21
+ const jsonData = JSON.parse(data);
22
+ const title = jsonData.title;
23
+ const articles = jsonData.articles;
24
+ f(title, articles);
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,141 @@
1
+ import Blog from "../Blog.js";
2
+ import Article from "../Article.js";
3
+ import { fetchData, postData } from "../model/APIModel.js";
4
+ import { server } from "./simpleServer.js";
5
+
6
+ function generateRandomContent(length) {
7
+ const char = "a";
8
+ let str = "";
9
+ for (let i = 0; i < length; i++) {
10
+ const rnd = Math.random() * 10;
11
+ const char1 = String.fromCharCode(char.charCodeAt(0) + rnd);
12
+ str += char1;
13
+ }
14
+ return str;
15
+ }
16
+
17
+ describe("File Model test", () => {
18
+ it("should load blog data from blog.json", async () => {
19
+ const blog = new Blog();
20
+ blog.setStyle(
21
+ "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
22
+ );
23
+
24
+ const content = generateRandomContent(200);
25
+
26
+ const article = new Article("hello", content);
27
+ blog.addArticle(article);
28
+
29
+ await blog.load("blog.json");
30
+
31
+ expect(await blog.toHTML()).toContain(content);
32
+ expect(blog).toBeDefined();
33
+ });
34
+ });
35
+
36
+ describe("API Model test", () => {
37
+ beforeAll(() => {
38
+ const port = 8081;
39
+ server.listen(port, () => {
40
+ console.log(`Simple server running at http://localhost:${port}/`);
41
+ });
42
+ });
43
+ afterAll(() => {
44
+ return new Promise((done) => {
45
+ server.close(done);
46
+ });
47
+ });
48
+
49
+ it("should load blog data from API", async () => {
50
+ const blog = new Blog();
51
+ blog.setAPI("http://localhost:8081/blog");
52
+ blog.setStyle(
53
+ "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
54
+ );
55
+ await blog.init();
56
+
57
+ const content = generateRandomContent(200);
58
+
59
+ await new Promise((resolve) => {
60
+ const req = {
61
+ on: (event, cb) => {
62
+ if (event === "data") cb(`title=hello&content=${content}`);
63
+ if (event === "end") cb();
64
+ },
65
+ };
66
+ const res = {
67
+ writeHead: () => {},
68
+ end: resolve,
69
+ };
70
+ blog.postArticle(req, res);
71
+ });
72
+
73
+ expect(await blog.toHTML()).toContain(content);
74
+ expect(blog).toBeDefined();
75
+ });
76
+
77
+ it("should fetch data from API", async () => {
78
+ const API = "http://localhost:8081/blog";
79
+ const data = await fetchData(API);
80
+ expect(data).toBeDefined();
81
+ expect(data.title).toBe("Mock Blog Title");
82
+ expect(data.articles).toHaveLength(2);
83
+ });
84
+
85
+ it("should post blog data to API", async () => {
86
+ const API = "http://localhost:8081/blog";
87
+ const newArticle = { title: "New Post", content: "New Content" };
88
+ const data = await postData(API, newArticle);
89
+ expect(data).toEqual(newArticle);
90
+ });
91
+ });
92
+
93
+ describe("Database Model test", () => {
94
+ it("should be empty", async () => {
95
+ const blog = new Blog();
96
+ blog.database.type = "sqlite";
97
+ blog.database.dbname = "test";
98
+ blog.setStyle(
99
+ "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
100
+ );
101
+
102
+ //const article = new Article("hello", "hello world1!");
103
+ //blog.postArticle(article);
104
+
105
+ //await blog.load("blog.json");
106
+
107
+ expect(await blog.toHTML()).not.toContain("<article>");
108
+ expect(blog).toBeDefined();
109
+ });
110
+
111
+ it("should load blog data from sqlite database", async () => {
112
+ const blog = new Blog();
113
+ blog.database.type = "sqlite";
114
+ blog.database.dbname = "test";
115
+ blog.setStyle(
116
+ "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
117
+ );
118
+ await blog.init();
119
+
120
+ const content = generateRandomContent(200);
121
+
122
+ await new Promise((resolve) => {
123
+ const req = {
124
+ on: (event, cb) => {
125
+ if (event === "data") cb(`title=hello&content=${content}`);
126
+ if (event === "end") cb();
127
+ },
128
+ };
129
+ const res = {
130
+ writeHead: () => {},
131
+ end: resolve,
132
+ };
133
+ blog.postArticle(req, res);
134
+ });
135
+
136
+ //await blog.load("blog.json");
137
+
138
+ expect(await blog.toHTML()).toContain(content);
139
+ expect(blog).toBeDefined();
140
+ });
141
+ });
@@ -0,0 +1,43 @@
1
+ import http from "http";
2
+
3
+ const port = 8081;
4
+
5
+ export const server = http.createServer((req, res) => {
6
+ // Set CORS headers to allow requests from different origins if needed
7
+ res.setHeader("Access-Control-Allow-Origin", "*");
8
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
9
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
10
+
11
+ if (req.method === "OPTIONS") {
12
+ res.writeHead(204);
13
+ res.end();
14
+ return;
15
+ }
16
+
17
+ if (req.url === "/blog") {
18
+ if (req.method === "GET") {
19
+ const data = {
20
+ title: "Mock Blog Title",
21
+ articles: [
22
+ { title: "Mock Article 1", content: "Content of mock article 1" },
23
+ { title: "Mock Article 2", content: "Content of mock article 2" },
24
+ ],
25
+ };
26
+ res.writeHead(200, { "Content-Type": "application/json" });
27
+ res.end(JSON.stringify(data));
28
+ } else if (req.method === "POST") {
29
+ let body = "";
30
+ req.on("data", (chunk) => (body += chunk.toString()));
31
+ req.on("end", () => {
32
+ res.writeHead(201, { "Content-Type": "application/json" });
33
+ res.end(body); // Echo the received data back
34
+ });
35
+ } else {
36
+ res.writeHead(405); // Method Not Allowed
37
+ res.end();
38
+ }
39
+ } else {
40
+ res.writeHead(404); // Not Found
41
+ res.end();
42
+ }
43
+ });