@lexho111/plainblog 0.0.8 → 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,8 +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";
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";
6
8
 
7
9
  export default class Blog {
8
10
  constructor() {
@@ -10,34 +12,22 @@ export default class Blog {
10
12
  this.articles = [];
11
13
  this.style = "body { font-family: Arial; }";
12
14
  this.filename = null;
13
- }
14
-
15
- async fetchDataFromApi(apiUrl) {
16
- try {
17
- const response = await fetch(apiUrl);
15
+ this.#server = null;
18
16
 
19
- if (!response.ok) {
20
- throw new Error(`HTTP error! status: ${response.status}`);
21
- }
22
-
23
- const data = await response.json();
24
- return data;
25
- } catch (error) {
26
- console.error("Failed to fetch data:", error);
27
- return null;
28
- }
17
+ this.database = {
18
+ type: "sqlite",
19
+ username: "user",
20
+ password: "password",
21
+ host: "localhost",
22
+ dbname: "blog",
23
+ };
29
24
  }
30
25
 
31
- async postDataToApi(apiUrl, postData) {
32
- const response = await fetch(apiUrl, {
33
- method: "POST",
34
- body: JSON.stringify(postData),
35
- headers: { "Content-Type": "application/json" },
36
- });
37
-
38
- const data = await response.json();
39
- return data;
40
- }
26
+ // Private fields
27
+ #server = null;
28
+ #databaseModel;
29
+ #isExternalAPI = false;
30
+ #apiUrl = "";
41
31
 
42
32
  setTitle(title) {
43
33
  this.title = title;
@@ -51,102 +41,184 @@ export default class Blog {
51
41
  this.style = style;
52
42
  }
53
43
 
54
- APIUrl = "";
44
+ /** initializes database */
45
+ async init() {
46
+ if (this.#isExternalAPI) {
47
+ await this.loadFromAPI();
48
+ } else {
49
+ console.log(`initialize ${this.database.type} database`);
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();
54
+ const responseData = { title: dbTitle, articles: dbArticles };
55
+ this.#applyBlogData(responseData);
56
+ }
57
+ }
55
58
 
56
- setAPI(APIUrl) {
57
- this.APIUrl = APIUrl;
59
+ /** post a blog article */
60
+ async postArticle(req, res) {
61
+ let body = "";
62
+ req.on("data", (chunk) => {
63
+ 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
+ }
83
+ }
84
+
85
+ res.writeHead(303, { Location: "/" });
86
+ res.end();
87
+ });
58
88
  }
59
89
 
90
+ /** start a http server with default port 8080 */
60
91
  async startServer(port = 8080) {
61
- if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
92
+ if (this.#databaseModel === undefined) await this.init(); // init blog if it didn't already happen
93
+ await this.#databaseModel.initialize();
94
+
62
95
  const server = http.createServer(async (req, res) => {
63
- if (req.method === "POST") {
64
- let body = "";
65
- req.on("data", (chunk) => {
66
- body += chunk.toString();
67
- });
68
- req.on("end", async () => {
69
- const params = new URLSearchParams(body);
70
- const title = params.get("title");
71
- const content = params.get("content");
72
-
73
- if (title && content) {
74
- const newArticle = new Article(title, content);
75
- this.addArticle(newArticle);
76
-
77
- // Post the new article to the backend API
78
- try {
79
- const postUrl = new URL(this.APIUrl);
80
- postUrl.pathname = "/blog/articles";
81
- await this.postDataToApi(postUrl.toString(), newArticle);
82
- } catch (error) {
83
- console.error("Failed to post new article to API:", error);
84
- }
85
- }
86
-
87
- // Redirect to the homepage to show the new article
88
- res.writeHead(303, { Location: "/" });
89
- res.end();
90
- });
91
- } else {
92
- const html = await this.toHTML();
96
+ // API routes
97
+ if (req.url.startsWith("/api")) {
98
+ await this.#jsonAPI(req, res);
99
+ return;
100
+ }
101
+
102
+ // Web Page Routes
103
+ // POST new article
104
+ if (req.method === "POST" && req.url === "/") {
105
+ await this.#databaseModel.updateBlogTitle(this.title);
106
+ await this.postArticle(req, res);
107
+ // GET artciles
108
+ } else if (req.method === "GET" && req.url === "/") {
109
+ // load articles
110
+
111
+ const html = await this.toHTML(); // render this blog to HTML
93
112
  res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
94
113
  res.end(html);
114
+ } else {
115
+ // Error 404
116
+ res.writeHead(404, { "Content-Type": "application/json" });
117
+ res.end(JSON.stringify({ message: "Not Found" }));
95
118
  }
96
119
  });
97
120
 
98
- server.listen(port, () => {
99
- console.log(`Server running at http://localhost:${port}/`);
121
+ this.#server = server;
122
+
123
+ return new Promise((resolve) => {
124
+ this.#server.listen(port, () => {
125
+ console.log(`Server running at http://localhost:${port}/`);
126
+ resolve(); // Resolve the promise when the server is listening
127
+ });
128
+ });
129
+ } // http server
130
+
131
+ async closeServer() {
132
+ return new Promise((resolve, reject) => {
133
+ if (this.#server) {
134
+ this.#server.close((err) => {
135
+ if (err) return reject(err);
136
+ console.log("Server closed.");
137
+ resolve();
138
+ });
139
+ } else {
140
+ resolve(); // Nothing to close
141
+ }
100
142
  });
101
143
  }
102
144
 
103
- async save(filename = this.filename) {
104
- if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
105
- if (!filename) {
106
- console.error("Error: Filename not provided and not set previously.");
107
- return;
145
+ /** Populates the blog's title and articles from a data object. */
146
+ #applyBlogData(data) {
147
+ this.articles = []; // Clear existing articles before loading new ones
148
+ this.setTitle(data.title);
149
+ // Assuming the API returns an array of objects with title and content
150
+ if (data.articles && Array.isArray(data.articles)) {
151
+ for (const articleData of data.articles) {
152
+ this.addArticle(new Article(articleData.title, articleData.content));
153
+ }
108
154
  }
109
- this.filename = filename;
110
- const data = {
111
- title: this.title,
112
- articles: this.articles,
113
- };
155
+ }
114
156
 
115
- try {
116
- await fs.writeFile(filename, JSON.stringify(data, null, 2));
117
- console.log(`Blog data saved to ${filename}`);
118
- } catch (err) {
119
- console.error("Error saving blog data:", err);
120
- }
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);
121
163
  }
122
164
 
123
165
  async load(filename) {
124
- this.filename = filename;
125
- const data = await fs.readFile(filename, "utf8");
126
- const jsonData = JSON.parse(data);
127
- this.title = jsonData.title;
128
- this.articles = jsonData.articles.map(
129
- (article) => new Article(article.title, article.content)
130
- );
166
+ loadFromFile(filename, (title, articles) => {
167
+ this.title = title;
168
+ this.articles = articles.map(
169
+ (article) => new Article(article.title, article.content)
170
+ );
171
+ });
131
172
  }
132
173
 
133
- async loadFromAPI(apiUrl) {
134
- const data = await this.fetchDataFromApi(apiUrl);
174
+ async loadFromAPI() {
175
+ const data = await fetchData(this.#apiUrl);
135
176
  if (data) {
136
- this.articles = []; // Clear existing articles before loading new ones
137
- this.setTitle(data.title);
138
- // Assuming the API returns an array of objects with title and content
139
- if (data.articles && Array.isArray(data.articles)) {
140
- for (const articleData of data.articles) {
141
- this.addArticle(new Article(articleData.title, articleData.content));
142
- }
143
- this.apiDataLoaded = true; // Mark that we have successfully loaded data
144
- }
177
+ this.#applyBlogData(data);
145
178
  }
146
179
  }
147
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
+ });
211
+ }
212
+ }
213
+
214
+ /** set external API */
215
+ setAPI(APIUrl) {
216
+ this.#apiUrl = APIUrl;
217
+ this.#isExternalAPI = true;
218
+ }
219
+
220
+ /** print markdown to the console */
148
221
  async print() {
149
- if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
150
222
  console.log(`# ${this.title}`);
151
223
  for (const article of this.articles) {
152
224
  console.log(`## ${article.title}`);
@@ -154,21 +226,17 @@ export default class Blog {
154
226
  }
155
227
  }
156
228
 
157
- async toHTML() {
158
- // If we have an API URL and haven't loaded data yet, load it now.
159
- if (this.APIUrl && !this.apiDataLoaded) {
160
- await this.loadFromAPI(this.APIUrl);
161
- }
162
-
229
+ /** format this blog content to html */
230
+ formatter(data) {
163
231
  return `<!DOCTYPE html>
164
232
  <html lang="de">
165
233
  <head>
166
234
  <meta charset="UTF-8">
167
- <title>${this.title}</title>
168
- <style>${this.style}</style>
235
+ <title>${data.title}</title>
236
+ <style>${data.style}</style>
169
237
  </head>
170
238
  <body>
171
- <h1>${this.title}</h1>
239
+ <h1>${data.title}</h1>
172
240
  <div style="width: 500px;">
173
241
  <form action="/" method="POST">
174
242
  <h3>Add a New Article</h3>
@@ -177,7 +245,7 @@ export default class Blog {
177
245
  <button type="submit">Add Article</button>
178
246
  </form>
179
247
  <hr>
180
- ${this.articles
248
+ ${data.articles
181
249
  .map(
182
250
  (article) => `
183
251
  <article>
@@ -190,4 +258,37 @@ export default class Blog {
190
258
  </body>
191
259
  </html>`;
192
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
+ }
193
294
  }
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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. Articles are stored in a file or a database specified via an API.
4
4
 
5
- ## Install
5
+ ## Installation
6
6
 
7
7
  ```
8
8
  npm install @lexho111/plainblog
@@ -11,7 +11,7 @@ npm install @lexho111/plainblog
11
11
  ## Quick Start
12
12
 
13
13
  ```
14
- import { Blog } from "@lexho111/plainblog";
14
+ import Blog from "@lexho111/plainblog";
15
15
 
16
16
  const blog = new Blog();
17
17
  blog.setTitle("My Blog");
@@ -24,49 +24,43 @@ Now you can start to add articles to your blog via your webbrowser on `http://lo
24
24
 
25
25
  ## More Features
26
26
 
27
- set an API to fetch data from an external database
27
+ **SQLite** is the default database. But you can use **PostgreSQL** instead.
28
28
 
29
- ```
30
- blog.setAPI("http://localhost:8081/blog")
31
- ```
32
-
33
- ### run api server with sqlite database
29
+ ### run api server with postgres database
34
30
 
35
31
  ```
36
- import { Blog, storageserver} from "@lexho111/plainblog";
37
- await storageserver("sqlite", 8081); // you can use a postgres db too
32
+ import Blog from "@lexho111/plainblog";
33
+
38
34
  const blog = new Blog();
39
- blog.setAPI(storageserver.getAPIURL());
35
+ blog.database.type = "postgres";
36
+ blog.database.user = "user";
37
+ blog.database.password = "password";
38
+ blog.database.host = "localhost";
40
39
  blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
40
+ await blog.init(); // load data from database
41
41
 
42
42
  blog.startServer(8080);
43
43
  ```
44
44
 
45
- ### run api server with postgres database
45
+ ### set an external API to fetch data from an external database
46
46
 
47
47
  ```
48
- docker run -p 5432:5432 --name postgresdb --restart always -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e POSTGRES_DB=blog -v db_data:/var/lib/postgresql/data -d postgres:13
48
+ blog.setAPI("http://example.com:5432/blog")
49
49
  ```
50
50
 
51
+ save data to file
52
+
51
53
  ```
52
- import { Blog, storageserver} from "@lexho111/plainblog";
54
+ import Blog from "@lexho111/plainblog";
55
+ import { Article } from "@lexho111/plainblog";
53
56
 
54
- storageserver.setUsername("username");
55
- storageserver.setPassword("password");
56
- storageserver.setHost("localhost");
57
- await storageserver.start("postgres", 8081);
58
57
  const blog = new Blog();
59
- blog.setAPI(storageserver.getAPIURL());
60
58
  blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
61
59
 
62
- blog.startServer(8080);
63
- ```
64
-
65
- save data to file
60
+ const article = new Article("hello", "hello world!");
61
+ blog.addArticle(article);
66
62
 
67
- ```
68
- # save your blog to 'myblog.json'
69
- await blog.save("myblog.json");
63
+ blog.save("blog.json");
70
64
 
71
65
  # load data from 'myblog.json'
72
66
  await blog.load("myblog.json");
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
+ }
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Blog from "./Blog.js";
2
2
  import Article from "./Article.js";
3
- import storageserver from "./api-server.js";
4
3
 
5
- export { Blog, Article, storageserver };
4
+ export { Blog, Article };
5
+ export default Blog;
@@ -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
+ }
@@ -0,0 +1,104 @@
1
+ import { Sequelize, DataTypes } from "sequelize";
2
+
3
+ export default class DatabaseModel {
4
+ #username;
5
+ #password;
6
+ #host;
7
+ #dbport = 5432;
8
+ #dbname = "blog";
9
+
10
+ #sequelize;
11
+ #Article;
12
+ #BlogInfo;
13
+
14
+ constructor(options) {
15
+ const databasetype = options.type; // the database type defines
16
+ if (databasetype === "sqlite") {
17
+ this.#sequelize = new Sequelize({
18
+ dialect: "sqlite",
19
+ storage: `./${this.#dbname}.db`,
20
+ logging: false,
21
+ });
22
+ } else if (databasetype === "postgres") {
23
+ this.#username = options.username;
24
+ this.#password = options.password;
25
+ this.#host = options.host;
26
+ if (options.dbname) this.#dbname = options.dbname;
27
+ if (!this.#username || !this.#password || !this.#host) {
28
+ throw new Error(
29
+ "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options."
30
+ );
31
+ }
32
+ console.log(
33
+ `postgres://${this.#username}:${this.#password}@${this.#host}:${
34
+ this.#dbport
35
+ }/${this.#dbname}`
36
+ );
37
+
38
+ this.#sequelize = new Sequelize(
39
+ `postgres://${this.#username}:${this.#password}@${this.#host}:${
40
+ this.#dbport
41
+ }/${this.#dbname}`,
42
+ { logging: false }
43
+ );
44
+ } else {
45
+ throw new Error(`Error! ${databasetype} is an unknown database type.`);
46
+ }
47
+
48
+ this.#Article = this.#sequelize.define(
49
+ "Article",
50
+ {
51
+ title: DataTypes.STRING,
52
+ content: DataTypes.STRING,
53
+ },
54
+ {
55
+ timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
56
+ }
57
+ );
58
+
59
+ this.#BlogInfo = this.#sequelize.define(
60
+ "BlogInfo",
61
+ {
62
+ title: DataTypes.STRING,
63
+ },
64
+ {
65
+ timestamps: false,
66
+ }
67
+ );
68
+ }
69
+
70
+ async initialize() {
71
+ // This creates the tables if they don't exist.
72
+ await this.#sequelize.sync({ alter: true });
73
+
74
+ // Check for and create the initial blog title right after syncing.
75
+ const blogInfoCount = await this.#BlogInfo.count();
76
+ if (blogInfoCount === 0) {
77
+ await this.#BlogInfo.create({ title: "My Default Blog Title" });
78
+ console.log("Initialized blog title in database.");
79
+ }
80
+ }
81
+
82
+ // model
83
+ async findAll() {
84
+ return this.#Article.findAll();
85
+ }
86
+
87
+ async save(newArticle) {
88
+ await this.#Article.create(newArticle);
89
+ console.log("Added new article:", newArticle);
90
+ }
91
+
92
+ async getBlogTitle() {
93
+ // Find the first (and only) entry in the BlogInfo table.
94
+ const blogInfo = await this.#BlogInfo.findOne();
95
+
96
+ return blogInfo.title;
97
+ }
98
+
99
+ async updateBlogTitle(newTitle) {
100
+ // Find the first (and only) entry and update its title.
101
+ // Using where: {} will always find the first row.
102
+ await this.#BlogInfo.update({ title: newTitle }, { where: {} });
103
+ }
104
+ }
@@ -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.8",
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",
@@ -7,7 +7,7 @@ test("get content", () => {
7
7
  expect(content).toContain("text text text");
8
8
  });
9
9
 
10
- test("get shortened content", () => {
10
+ test("get shortened content 1", () => {
11
11
  const article = new Article("title", "text text text");
12
12
  const content = article.getContentShort();
13
13
  expect(article.title).toContain("title");
@@ -16,7 +16,7 @@ test("get shortened content", () => {
16
16
  expect(content).not.toContain("...");
17
17
  });
18
18
 
19
- test("get shortened content", () => {
19
+ test("get shortened content 2", () => {
20
20
  let text = "text";
21
21
  for (let i = 0; i < 500; i++) {
22
22
  text += " ";
@@ -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
+ });
@@ -1,29 +1,52 @@
1
- import { start as startAPIServer } from "../api-server.js";
2
1
  import fetch from "node-fetch";
2
+ import Blog from "../Blog.js";
3
3
 
4
- describe("API Server Health Check", () => {
5
- let server;
6
- const PORT = 8085;
7
- const apiBaseUrl = `http://localhost:${PORT}`;
4
+ /** this is a test for
5
+ * import Blog from "@lexho111/plainblog";
6
+ *
7
+ * const blog = new Blog();
8
+ * blog.setTitle("My Blog");
9
+ * blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
10
+ *
11
+ * blog.startServer(8080);
12
+ */
8
13
 
14
+ const PORT = 8080;
15
+ const apiBaseUrl = `http://localhost:${PORT}`;
16
+ describe("server test", () => {
17
+ const blog = new Blog(); // Create one blog instance for all tests
18
+ blog.setTitle("My Blog");
19
+
20
+ // Use beforeAll to set up and start the server once before any tests run
9
21
  beforeAll(async () => {
10
- // how about to use in-memory database?
11
- server = await startAPIServer("sqlite", PORT);
22
+ //blog.database.type = "sqlite";
23
+ blog.setStyle(
24
+ "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
25
+ );
26
+ // Await it to ensure the server is running before tests start.
27
+ await blog.startServer(PORT);
12
28
  });
13
29
 
14
- afterAll((done) => {
15
- server.close(() => {
16
- done();
17
- });
30
+ // Use afterAll to shut down the server once after all tests have finished
31
+ afterAll(async () => {
32
+ // Await the closing of the server to ensure a clean shutdown.
33
+ await blog.closeServer();
18
34
  });
19
35
 
20
- test("should respond with 200 OK on the /health endpoint", async () => {
21
- const response = await fetch(`${apiBaseUrl}/health`);
36
+ test("should post a new article via /api/articles endpoint", async () => {
37
+ const newArticle = {
38
+ title: "Test Title from Jest",
39
+ content: "This is the content of the test article.",
40
+ };
22
41
 
23
- // Check for a successful status code
24
- expect(response.status).toBe(200);
42
+ const response = await fetch(`${apiBaseUrl}/api/articles`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify(newArticle),
46
+ });
25
47
 
26
- const body = await response.json();
27
- expect(body).toEqual({ status: "ok" });
48
+ expect(response.status).toBe(201);
49
+ const responseData = await response.json();
50
+ expect(responseData).toEqual(newArticle);
28
51
  });
29
52
  });
@@ -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
+ });
package/api-server.js DELETED
@@ -1,175 +0,0 @@
1
- import http from "http";
2
- import { Sequelize, DataTypes } from "sequelize";
3
-
4
- // This is a simple API server that provides blog data.
5
- // This is the data that our API will serve, similar to a myblog.json file.
6
- const blogData = {
7
- title: "My Blog from the API",
8
- articles: [
9
- {
10
- title: "First API Post",
11
- content: "This article was loaded from our new external API server!",
12
- },
13
- {
14
- title: "REST is Cool",
15
- content:
16
- "Using a REST API allows us to separate our frontend from our backend.",
17
- },
18
- ],
19
- };
20
-
21
- let username;
22
- let password;
23
- let host;
24
- const dbport = 5432;
25
- const dbname = process.env.POSTGRES_DB || "blog";
26
-
27
- export function setUsername(username1) {
28
- username = username1;
29
- }
30
- export function setPassword(password1) {
31
- password = password1;
32
- }
33
- export function setHost(host1) {
34
- host = host1;
35
- }
36
-
37
- let serverPort = 8081; // Default port
38
-
39
- export async function start(databasetype, port = 8081) {
40
- let sequelize;
41
- serverPort = port; // Update the port for getAPIURL
42
-
43
- if (databasetype === "sqlite") {
44
- sequelize = new Sequelize({
45
- dialect: "sqlite",
46
- storage: "./blog.db",
47
- logging: false,
48
- });
49
- } else if (databasetype === "postgres") {
50
- if (!username || !password || !host) {
51
- throw new Error(
52
- "PostgreSQL credentials not set. Please use setUsername(), setPassword(), and setHost() before starting the server."
53
- );
54
- }
55
-
56
- sequelize = new Sequelize(
57
- `postgres://${username}:${password}@${host}:${dbport}/${dbname}`,
58
- { logging: false }
59
- );
60
- }
61
-
62
- const Article = sequelize.define(
63
- "Article",
64
- {
65
- title: DataTypes.STRING,
66
- content: DataTypes.STRING,
67
- },
68
- {
69
- timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
70
- }
71
- );
72
-
73
- try {
74
- // The connectWithRetry logic is now integrated here and properly awaited.
75
- let retries = 5;
76
- while (retries) {
77
- try {
78
- await sequelize.authenticate();
79
- console.log("Connection has been established successfully.");
80
-
81
- // Sync all models and populate data
82
- await sequelize.sync({ alter: true });
83
- console.log("All models were synchronized successfully.");
84
-
85
- if ((await Article.count()) === 0) {
86
- for (const articleData of blogData.articles) {
87
- await Article.create(articleData);
88
- }
89
- console.log("Initial blog data populated.");
90
- }
91
- break; // Success, exit retry loop
92
- } catch (err) {
93
- // For sqlite, we don't need to retry, just throw the error.
94
- if (databasetype === "sqlite") throw err;
95
-
96
- console.error("Unable to connect to the database:", err.name);
97
- retries -= 1;
98
- console.log(`Retries left: ${retries}`);
99
- if (retries === 0) throw err; // Throw error if max retries reached
100
- await new Promise((res) => setTimeout(res, 5000));
101
- }
102
- }
103
-
104
- const server = http.createServer(async (req, res) => {
105
- // Set CORS headers for all responses
106
- res.setHeader("Access-Control-Allow-Origin", "*");
107
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
108
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
109
-
110
- // Handle preflight CORS requests
111
- if (req.method === "OPTIONS") {
112
- res.writeHead(204);
113
- res.end();
114
- return;
115
- }
116
-
117
- // Health check endpoint
118
- if (req.method === "GET" && req.url === "/health") {
119
- res.writeHead(200, { "Content-Type": "application/json" });
120
- res.end(JSON.stringify({ status: "ok" }));
121
- return;
122
- }
123
-
124
- // GET all blog data
125
- if (req.method === "GET" && req.url === "/blog") {
126
- res.writeHead(200, { "Content-Type": "application/json" });
127
- const dbArticles = await Article.findAll();
128
- const responseData = {
129
- title: blogData.title, // Keep the title from the original constant
130
- articles: dbArticles,
131
- };
132
- res.end(JSON.stringify(responseData));
133
- }
134
- // POST a new article
135
- else if (req.method === "POST" && req.url === "/blog/articles") {
136
- let body = "";
137
- req.on("data", (chunk) => (body += chunk.toString()));
138
- req.on("end", async () => {
139
- const newArticle = JSON.parse(body);
140
- await Article.create(newArticle);
141
- console.log("Added new article:", newArticle);
142
- res.writeHead(201, { "Content-Type": "application/json" });
143
- res.end(JSON.stringify(newArticle));
144
- });
145
- } else {
146
- res.writeHead(404, { "Content-Type": "application/json" });
147
- res.end(JSON.stringify({ message: "Not Found" }));
148
- }
149
- });
150
-
151
- return new Promise((resolve) => {
152
- server.listen(port, () => {
153
- console.log(`API server running at http://localhost:${port}/`);
154
- resolve(server);
155
- });
156
- });
157
- } catch (error) {
158
- console.error("Failed to initialize database or start server:", error);
159
- process.exit(1); // Exit the process if there's a critical error
160
- }
161
- }
162
-
163
- function getAPIURL() {
164
- return `http://localhost:${serverPort}/blog`;
165
- }
166
-
167
- const storageserver = {
168
- start,
169
- getAPIURL,
170
- setUsername,
171
- setPassword,
172
- setHost,
173
- };
174
-
175
- export default storageserver;