@lexho111/plainblog 0.0.10 → 0.0.12
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 +120 -97
- package/Formatter.js +65 -0
- package/README.md +8 -1
- package/blog +0 -0
- package/blog.db +0 -0
- package/model/APIModel.js +23 -12
- package/model/DatabaseModel.js +6 -3
- package/package.json +2 -1
- package/scripts.min.js +2 -0
- package/styles.min.css +1 -0
- package/test/blog.test.js +7 -5
- package/test/model.test.js +17 -7
- package/test/server.test.js +29 -2
- package/test_1766398288732 +0 -0
- package/test_1766400137370.db +0 -0
- package/test_1766400274284.db +0 -0
- package/test_1766400505629.db +0 -0
- package/test_1766400546097.db +0 -0
- package/test_1766400566357.db +0 -0
- package/test_1766400610192.db +0 -0
package/Blog.js
CHANGED
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
import http from "http";
|
|
2
|
-
import
|
|
2
|
+
import fs from "fs";
|
|
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";
|
|
11
|
+
import pkg from "./package.json" with { type: "json" };;
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
8
14
|
|
|
9
15
|
export default class Blog {
|
|
10
16
|
constructor() {
|
|
11
17
|
this.title = "";
|
|
12
18
|
this.articles = [];
|
|
13
|
-
this.
|
|
19
|
+
this.styles = "";
|
|
14
20
|
this.filename = null;
|
|
15
21
|
this.#server = null;
|
|
16
22
|
|
|
17
23
|
this.database = {
|
|
18
24
|
type: "sqlite",
|
|
19
|
-
username: "
|
|
20
|
-
password: "
|
|
25
|
+
username: "user1",
|
|
26
|
+
password: "password1",
|
|
21
27
|
host: "localhost",
|
|
22
28
|
dbname: "blog",
|
|
23
29
|
};
|
|
30
|
+
|
|
31
|
+
const version = pkg.version;
|
|
32
|
+
console.log(`version: ${version}`);
|
|
33
|
+
|
|
34
|
+
this.loadScripts();
|
|
35
|
+
|
|
36
|
+
if(this.styles.length == 0) { // no style override specified via index.js
|
|
37
|
+
this.styles = "body { font-family: Arial; }"; // apply default style
|
|
38
|
+
this.loadStyles();
|
|
39
|
+
}
|
|
40
|
+
//console.log(styles)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
loadScripts() {
|
|
44
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
45
|
+
const __dirname = path.dirname(__filename);
|
|
46
|
+
this.scripts = "";
|
|
47
|
+
try {
|
|
48
|
+
console.log("load scripts")
|
|
49
|
+
this.scripts = fs.readFileSync(
|
|
50
|
+
path.join(__dirname, "scripts.min.js"),
|
|
51
|
+
"utf-8"
|
|
52
|
+
);
|
|
53
|
+
} catch(err) {
|
|
54
|
+
console.error("no scripts.min.js file found")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
loadStyles() {
|
|
59
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
60
|
+
const __dirname = path.dirname(__filename);
|
|
61
|
+
try {
|
|
62
|
+
this.styles = fs.readFileSync(
|
|
63
|
+
path.join(__dirname, "styles.min.css"), "utf-8");
|
|
64
|
+
} catch(err) {
|
|
65
|
+
console.error("no styles.min.css file found")
|
|
66
|
+
}
|
|
24
67
|
}
|
|
25
68
|
|
|
26
69
|
// Private fields
|
|
@@ -38,59 +81,82 @@ export default class Blog {
|
|
|
38
81
|
}
|
|
39
82
|
|
|
40
83
|
setStyle(style) {
|
|
41
|
-
this.
|
|
84
|
+
this.styles = style;
|
|
42
85
|
}
|
|
43
86
|
|
|
44
87
|
/** initializes database */
|
|
45
88
|
async init() {
|
|
89
|
+
try {
|
|
90
|
+
this.styles = fs.readFileSync(
|
|
91
|
+
new URL("./styles.min.css", import.meta.url),
|
|
92
|
+
"utf-8"
|
|
93
|
+
);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error("Failed to load styles.min.css:", err);
|
|
96
|
+
}
|
|
46
97
|
if (this.#isExternalAPI) {
|
|
98
|
+
console.log("external API");
|
|
47
99
|
await this.loadFromAPI();
|
|
48
100
|
} else {
|
|
49
|
-
console.log(`
|
|
101
|
+
console.log(`database: ${this.database.type}`);
|
|
50
102
|
this.#databaseModel = new DatabaseModel(this.database);
|
|
103
|
+
console.log(`connected to database`);
|
|
51
104
|
await this.#databaseModel.initialize();
|
|
52
105
|
const dbTitle = await this.#databaseModel.getBlogTitle();
|
|
53
106
|
const dbArticles = await this.#databaseModel.findAll();
|
|
54
|
-
|
|
107
|
+
let title = "";
|
|
108
|
+
if (this.title.length > 0) title = this.title; // use blog title if set
|
|
109
|
+
else title = dbTitle;
|
|
110
|
+
const responseData = { title: title, articles: dbArticles };
|
|
55
111
|
this.#applyBlogData(responseData);
|
|
56
112
|
}
|
|
57
113
|
}
|
|
58
114
|
|
|
59
115
|
/** post a blog article */
|
|
60
116
|
async postArticle(req, res) {
|
|
61
|
-
|
|
117
|
+
// OLD CODE
|
|
118
|
+
/*let body = "";
|
|
62
119
|
req.on("data", (chunk) => {
|
|
63
120
|
body += chunk.toString();
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
});*/
|
|
122
|
+
// NEW CODE
|
|
123
|
+
// Create a stream of the body string
|
|
124
|
+
const body$ = fromEvent(req, "data").pipe(
|
|
125
|
+
map((chunk) => chunk.toString()),
|
|
126
|
+
scan((acc, chunk) => acc + chunk, ""), // Accumulate chunks
|
|
127
|
+
takeUntil(fromEvent(req, "end")), // Stop when 'end' emits
|
|
128
|
+
last(), // Emit only the final full string
|
|
129
|
+
defaultIfEmpty("") // Handle empty bodies
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const body = await firstValueFrom(body$);
|
|
133
|
+
|
|
134
|
+
//req.on("end", async () => {
|
|
135
|
+
const params = new URLSearchParams(body);
|
|
136
|
+
const title = params.get("title");
|
|
137
|
+
const content = params.get("content");
|
|
138
|
+
|
|
139
|
+
if (title && content) {
|
|
140
|
+
const newArticleData = { title, content };
|
|
141
|
+
try {
|
|
142
|
+
// Save the new article to the database via the ApiServer
|
|
143
|
+
if (this.#databaseModel) await this.#databaseModel.save(newArticleData);
|
|
144
|
+
if (this.#isExternalAPI) await postData(this.#apiUrl, newArticleData);
|
|
145
|
+
// Add the article to the local list for immediate display
|
|
146
|
+
this.addArticle(new Article(title, content));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error("Failed to post new article to API:", error);
|
|
83
149
|
}
|
|
150
|
+
}
|
|
84
151
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
152
|
+
res.writeHead(303, { Location: "/" });
|
|
153
|
+
res.end();
|
|
154
|
+
//});
|
|
88
155
|
}
|
|
89
156
|
|
|
90
157
|
/** start a http server with default port 8080 */
|
|
91
158
|
async startServer(port = 8080) {
|
|
92
159
|
if (this.#databaseModel === undefined) await this.init(); // init blog if it didn't already happen
|
|
93
|
-
await this.#databaseModel.initialize();
|
|
94
160
|
|
|
95
161
|
const server = http.createServer(async (req, res) => {
|
|
96
162
|
// API routes
|
|
@@ -108,6 +174,8 @@ export default class Blog {
|
|
|
108
174
|
} else if (req.method === "GET" && req.url === "/") {
|
|
109
175
|
// load articles
|
|
110
176
|
|
|
177
|
+
this.loadScripts(); this.loadStyles();
|
|
178
|
+
|
|
111
179
|
const html = await this.toHTML(); // render this blog to HTML
|
|
112
180
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
113
181
|
res.end(html);
|
|
@@ -122,7 +190,7 @@ export default class Blog {
|
|
|
122
190
|
|
|
123
191
|
return new Promise((resolve) => {
|
|
124
192
|
this.#server.listen(port, () => {
|
|
125
|
-
console.log(`
|
|
193
|
+
console.log(`server running at http://localhost:${port}/`);
|
|
126
194
|
resolve(); // Resolve the promise when the server is listening
|
|
127
195
|
});
|
|
128
196
|
});
|
|
@@ -181,8 +249,16 @@ export default class Blog {
|
|
|
181
249
|
// controller
|
|
182
250
|
/** everything that happens in /api */
|
|
183
251
|
async #jsonAPI(req, res) {
|
|
252
|
+
if(req.method === "GET") {
|
|
253
|
+
if(req.url === "/api" || req.url === "/api/") {
|
|
254
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
255
|
+
const data = {
|
|
256
|
+
title: this.title
|
|
257
|
+
}
|
|
258
|
+
res.end(JSON.stringify(data));
|
|
259
|
+
}
|
|
184
260
|
// GET all blog data
|
|
185
|
-
if (req.
|
|
261
|
+
if (req.url === "/api/articles") {
|
|
186
262
|
// controller
|
|
187
263
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
188
264
|
const dbArticles = await this.#databaseModel.findAll();
|
|
@@ -195,7 +271,7 @@ export default class Blog {
|
|
|
195
271
|
}
|
|
196
272
|
|
|
197
273
|
// POST a new article
|
|
198
|
-
|
|
274
|
+
} else if (req.method === "POST" && req.url === "/api/articles") {
|
|
199
275
|
let body = "";
|
|
200
276
|
req.on("data", (chunk) => (body += chunk.toString()));
|
|
201
277
|
req.on("end", async () => {
|
|
@@ -219,76 +295,23 @@ export default class Blog {
|
|
|
219
295
|
|
|
220
296
|
/** print markdown to the console */
|
|
221
297
|
async print() {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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!");
|
|
298
|
+
const data = {
|
|
299
|
+
title: this.title,
|
|
300
|
+
articles: this.articles,
|
|
301
|
+
};
|
|
302
|
+
const markdown = formatMarkdown(data);
|
|
303
|
+
console.log(markdown);
|
|
283
304
|
}
|
|
284
305
|
|
|
285
306
|
/** render this blog content to valid html */
|
|
286
307
|
toHTML() {
|
|
287
308
|
const data = {
|
|
288
309
|
title: this.title,
|
|
289
|
-
style: this.style,
|
|
290
310
|
articles: this.articles,
|
|
291
311
|
};
|
|
292
|
-
|
|
312
|
+
|
|
313
|
+
const html = formatHTML(data, this.scripts, this.styles);
|
|
314
|
+
if (validate(html)) return html;
|
|
315
|
+
else throw new Error("Error. Invalid HTML!");
|
|
293
316
|
}
|
|
294
317
|
}
|
package/Formatter.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** format content to html */
|
|
2
|
+
export function formatHTML(data, script, style) {
|
|
3
|
+
//export function formatHTML(data) {
|
|
4
|
+
const button = `<button type="button" onClick="fillWithContent();" style="margin: 4px;">generate random text</button>`;
|
|
5
|
+
return `<!DOCTYPE html>
|
|
6
|
+
<html lang="de">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<title>${data.title}</title>
|
|
10
|
+
<style>${style}</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<h1>${data.title}</h1>
|
|
14
|
+
<div style="width: 500px;">
|
|
15
|
+
<form action="/" method="POST">
|
|
16
|
+
<h3>Add a New Article</h3>
|
|
17
|
+
<input type="text" id="title" name="title" placeholder="Article Title" required style="display: block; width: 300px; margin-bottom: 10px;">
|
|
18
|
+
<textarea id="content" name="content" placeholder="Article Content" required style="display: block; width: 300px; height: 100px; margin-bottom: 10px;"></textarea>
|
|
19
|
+
<button type="submit">Add Article</button>${button}
|
|
20
|
+
<script>
|
|
21
|
+
${script}
|
|
22
|
+
</script>
|
|
23
|
+
</form>
|
|
24
|
+
<hr>
|
|
25
|
+
${data.articles
|
|
26
|
+
.map(
|
|
27
|
+
(article) => `
|
|
28
|
+
<article style="overflow-wrap: break-word;">
|
|
29
|
+
<h2>${article.title}</h2>
|
|
30
|
+
<p>${article.getContentShort()}</p>
|
|
31
|
+
</article>`
|
|
32
|
+
)
|
|
33
|
+
.join("")}
|
|
34
|
+
</div>
|
|
35
|
+
</body>
|
|
36
|
+
</html>`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatMarkdown(data) {
|
|
40
|
+
let markdown = "";
|
|
41
|
+
markdown += `# ${data.title}\n`;
|
|
42
|
+
for (const article of data.articles) {
|
|
43
|
+
markdown += `## ${article.title}\n`;
|
|
44
|
+
markdown += article.content;
|
|
45
|
+
markdown += "\n";
|
|
46
|
+
}
|
|
47
|
+
return markdown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validate(html) {
|
|
51
|
+
let test = true; // all tests passed
|
|
52
|
+
if (!(html.includes("<html") && html.includes("</html"))) {
|
|
53
|
+
console.error("html not ok");
|
|
54
|
+
test = false;
|
|
55
|
+
}
|
|
56
|
+
if (!(html.includes("<head") && html.includes("</head"))) {
|
|
57
|
+
console.error("head not ok");
|
|
58
|
+
test = false;
|
|
59
|
+
}
|
|
60
|
+
if (!(html.includes("<body") && html.includes("</body"))) {
|
|
61
|
+
console.error("body not ok");
|
|
62
|
+
test = false;
|
|
63
|
+
}
|
|
64
|
+
return test;
|
|
65
|
+
}
|
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
|
|
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
ADDED
|
Binary 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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 */
|
package/model/DatabaseModel.js
CHANGED
|
@@ -14,9 +14,11 @@ export default class DatabaseModel {
|
|
|
14
14
|
constructor(options) {
|
|
15
15
|
const databasetype = options.type; // the database type defines
|
|
16
16
|
if (databasetype === "sqlite") {
|
|
17
|
+
// Use the full path for the database file from the options.
|
|
18
|
+
if (options.dbname) this.#dbname = options.dbname;
|
|
17
19
|
this.#sequelize = new Sequelize({
|
|
18
20
|
dialect: "sqlite",
|
|
19
|
-
storage:
|
|
21
|
+
storage: this.#dbname + ".db",
|
|
20
22
|
logging: false,
|
|
21
23
|
});
|
|
22
24
|
} else if (databasetype === "postgres") {
|
|
@@ -49,7 +51,7 @@ export default class DatabaseModel {
|
|
|
49
51
|
"Article",
|
|
50
52
|
{
|
|
51
53
|
title: DataTypes.STRING,
|
|
52
|
-
content: DataTypes.
|
|
54
|
+
content: DataTypes.TEXT,
|
|
53
55
|
},
|
|
54
56
|
{
|
|
55
57
|
timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
|
|
@@ -70,12 +72,13 @@ export default class DatabaseModel {
|
|
|
70
72
|
async initialize() {
|
|
71
73
|
// This creates the tables if they don't exist.
|
|
72
74
|
await this.#sequelize.sync({ alter: true });
|
|
75
|
+
console.log("database tables synced and ready.");
|
|
73
76
|
|
|
74
77
|
// Check for and create the initial blog title right after syncing.
|
|
75
78
|
const blogInfoCount = await this.#BlogInfo.count();
|
|
76
79
|
if (blogInfoCount === 0) {
|
|
77
80
|
await this.#BlogInfo.create({ title: "My Default Blog Title" });
|
|
78
|
-
console.log("
|
|
81
|
+
console.log("initialized blog title in database.");
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
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/scripts.min.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! gruntproject 2025-12-21 */
|
|
2
|
+
function generateRandomContent(t){let n="";for(let e=0;e<t;e++){var o=Math.random()*("z".charCodeAt(0)-"A".charCodeAt(0)),o=String.fromCharCode("A".charCodeAt(0)+o);n+=o}return n}function fillWithContent(){var e=document.getElementById("title");document.getElementById("content").value=generateRandomContent(200),e.value=generateRandomContent(50)}
|
package/styles.min.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
body{font-family:Courier;background-color:#d3d3d3}div{color:#000;width:500px}h1,h2,h3{font-weight:700}
|
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).
|
|
78
|
-
|
|
79
|
-
expect(
|
|
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();
|
package/test/model.test.js
CHANGED
|
@@ -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
|
+
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")
|
|
63
|
-
|
|
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 = "
|
|
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")
|
|
126
|
-
|
|
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: () => {},
|
package/test/server.test.js
CHANGED
|
@@ -11,8 +11,9 @@ import Blog from "../Blog.js";
|
|
|
11
11
|
* blog.startServer(8080);
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const PORT =
|
|
14
|
+
const PORT = 8090;
|
|
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("
|
|
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
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|