@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 +208 -107
- package/README.md +20 -26
- package/blog.db +0 -0
- package/blog.json +9 -0
- package/index.js +2 -2
- package/model/APIModel.js +30 -0
- package/model/DatabaseModel.js +104 -0
- package/model/fileModel.js +25 -0
- package/package.json +1 -1
- package/test/article.test.js +2 -2
- package/test/model.test.js +141 -0
- package/test/server.test.js +40 -17
- package/test/simpleServer.js +43 -0
- package/api-server.js +0 -175
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
req
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
const data = {
|
|
111
|
-
title: this.title,
|
|
112
|
-
articles: this.articles,
|
|
113
|
-
};
|
|
155
|
+
}
|
|
114
156
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
134
|
-
const data = await this
|
|
174
|
+
async loadFromAPI() {
|
|
175
|
+
const data = await fetchData(this.#apiUrl);
|
|
135
176
|
if (data) {
|
|
136
|
-
this
|
|
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
|
-
|
|
158
|
-
|
|
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>${
|
|
168
|
-
<style>${
|
|
235
|
+
<title>${data.title}</title>
|
|
236
|
+
<style>${data.style}</style>
|
|
169
237
|
</head>
|
|
170
238
|
<body>
|
|
171
|
-
<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
|
-
${
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
32
|
+
import Blog from "@lexho111/plainblog";
|
|
33
|
+
|
|
38
34
|
const blog = new Blog();
|
|
39
|
-
blog.
|
|
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
|
-
###
|
|
45
|
+
### set an external API to fetch data from an external database
|
|
46
46
|
|
|
47
47
|
```
|
|
48
|
-
|
|
48
|
+
blog.setAPI("http://example.com:5432/blog")
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
save data to file
|
|
52
|
+
|
|
51
53
|
```
|
|
52
|
-
import
|
|
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
|
-
|
|
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
package/index.js
CHANGED
|
@@ -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
package/test/article.test.js
CHANGED
|
@@ -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
|
+
});
|
package/test/server.test.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
//
|
|
11
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
21
|
-
const
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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;
|