@lexho111/plainblog 0.0.8 → 0.0.9
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 +177 -60
- package/README.md +20 -26
- package/blog.db +0 -0
- package/index.js +2 -2
- package/model.js +104 -0
- package/package.json +1 -1
- package/test/article.test.js +2 -2
- package/test/server.test.js +40 -17
- package/api-server.js +0 -175
package/Blog.js
CHANGED
|
@@ -3,6 +3,7 @@ import fetch from "node-fetch";
|
|
|
3
3
|
import { promises as fs } from "fs";
|
|
4
4
|
import { URLSearchParams } from "url";
|
|
5
5
|
import Article from "./Article.js";
|
|
6
|
+
import ApiServer from "./model.js";
|
|
6
7
|
|
|
7
8
|
export default class Blog {
|
|
8
9
|
constructor() {
|
|
@@ -10,9 +11,59 @@ export default class Blog {
|
|
|
10
11
|
this.articles = [];
|
|
11
12
|
this.style = "body { font-family: Arial; }";
|
|
12
13
|
this.filename = null;
|
|
14
|
+
this.#server = null;
|
|
15
|
+
|
|
16
|
+
this.database = {
|
|
17
|
+
type: "sqlite",
|
|
18
|
+
username: "user",
|
|
19
|
+
password: "password",
|
|
20
|
+
host: "localhost",
|
|
21
|
+
dbname: "blog",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Private fields
|
|
26
|
+
#server = null;
|
|
27
|
+
#apiServer;
|
|
28
|
+
#isExternalAPI = false;
|
|
29
|
+
#apiDataLoaded = false;
|
|
30
|
+
|
|
31
|
+
setTitle(title) {
|
|
32
|
+
this.title = title;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
addArticle(article) {
|
|
36
|
+
this.articles.push(article);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setStyle(style) {
|
|
40
|
+
this.style = style;
|
|
13
41
|
}
|
|
14
42
|
|
|
15
|
-
|
|
43
|
+
/** initializes database */
|
|
44
|
+
async init() {
|
|
45
|
+
if (this.#isExternalAPI) {
|
|
46
|
+
await this.loadFromAPI(this.#APIUrl);
|
|
47
|
+
} else {
|
|
48
|
+
console.log(`initialize ${this.database.type} database`);
|
|
49
|
+
this.#apiServer = new ApiServer(this.database);
|
|
50
|
+
const dbTitle = await this.#apiServer.getBlogTitle();
|
|
51
|
+
const dbArticles = await this.#apiServer.findAll();
|
|
52
|
+
const responseData = { title: dbTitle, articles: dbArticles };
|
|
53
|
+
this.#loadFromData(responseData);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// EXTERNAL DATA API
|
|
58
|
+
#APIUrl = "";
|
|
59
|
+
|
|
60
|
+
setAPI(APIUrl) {
|
|
61
|
+
this.#APIUrl = APIUrl;
|
|
62
|
+
this.#isExternalAPI = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** fetch data from an URL */
|
|
66
|
+
async #fetchDataFromApi(apiUrl) {
|
|
16
67
|
try {
|
|
17
68
|
const response = await fetch(apiUrl);
|
|
18
69
|
|
|
@@ -28,7 +79,8 @@ export default class Blog {
|
|
|
28
79
|
}
|
|
29
80
|
}
|
|
30
81
|
|
|
31
|
-
|
|
82
|
+
/** post data to an URL */
|
|
83
|
+
async #postDataToApi(apiUrl, postData) {
|
|
32
84
|
const response = await fetch(apiUrl, {
|
|
33
85
|
method: "POST",
|
|
34
86
|
body: JSON.stringify(postData),
|
|
@@ -39,69 +91,127 @@ export default class Blog {
|
|
|
39
91
|
return data;
|
|
40
92
|
}
|
|
41
93
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
/** post a blog article */
|
|
95
|
+
async #postArticle(req, res) {
|
|
96
|
+
let body = "";
|
|
97
|
+
req.on("data", (chunk) => {
|
|
98
|
+
body += chunk.toString();
|
|
99
|
+
});
|
|
45
100
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
req.on("end", async () => {
|
|
102
|
+
const params = new URLSearchParams(body);
|
|
103
|
+
const title = params.get("title");
|
|
104
|
+
const content = params.get("content");
|
|
49
105
|
|
|
50
|
-
|
|
51
|
-
|
|
106
|
+
if (title && content) {
|
|
107
|
+
const newArticleData = { title, content };
|
|
108
|
+
try {
|
|
109
|
+
// Save the new article to the database via the ApiServer
|
|
110
|
+
await this.#apiServer.save(newArticleData);
|
|
111
|
+
// Add the article to the local list for immediate display
|
|
112
|
+
this.addArticle(new Article(title, content));
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error("Failed to post new article to API:", error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
res.writeHead(303, { Location: "/" });
|
|
119
|
+
res.end();
|
|
120
|
+
});
|
|
52
121
|
}
|
|
53
122
|
|
|
54
|
-
|
|
123
|
+
/** everything that happens in /api */
|
|
124
|
+
async #jsonAPI(req, res) {
|
|
125
|
+
// GET all blog data
|
|
126
|
+
if (req.method === "GET" && req.url === "/api") {
|
|
127
|
+
// controller
|
|
128
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
129
|
+
const dbArticles = await this.#apiServer.findAll();
|
|
130
|
+
const responseData = {
|
|
131
|
+
title: this.title, // Keep the title from the original constant
|
|
132
|
+
articles: dbArticles,
|
|
133
|
+
};
|
|
134
|
+
console.log(`responseData: ${responseData}`);
|
|
135
|
+
res.end(JSON.stringify(responseData));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// POST a new article
|
|
139
|
+
else if (req.method === "POST" && req.url === "/api/articles") {
|
|
140
|
+
let body = "";
|
|
141
|
+
req.on("data", (chunk) => (body += chunk.toString()));
|
|
142
|
+
req.on("end", async () => {
|
|
143
|
+
const newArticle = JSON.parse(body);
|
|
55
144
|
|
|
56
|
-
|
|
57
|
-
|
|
145
|
+
// local
|
|
146
|
+
await this.#apiServer.save(newArticle); // --> to api server
|
|
147
|
+
// extern
|
|
148
|
+
|
|
149
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
150
|
+
res.end(JSON.stringify(newArticle));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
58
153
|
}
|
|
59
154
|
|
|
155
|
+
/** start a http server with default port 8080 */
|
|
60
156
|
async startServer(port = 8080) {
|
|
61
|
-
if (this
|
|
157
|
+
if (this.#apiServer === undefined) this.init(); // init blog if it didn't already happen
|
|
158
|
+
await this.#apiServer.initialize();
|
|
159
|
+
|
|
62
160
|
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();
|
|
161
|
+
// API routes
|
|
162
|
+
if (req.url.startsWith("/api")) {
|
|
163
|
+
await this.#jsonAPI(req, res);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Web Page Routes
|
|
168
|
+
// POST new article
|
|
169
|
+
if (req.method === "POST" && req.url === "/") {
|
|
170
|
+
await this.#apiServer.updateBlogTitle(this.title);
|
|
171
|
+
await this.#postArticle(req, res);
|
|
172
|
+
// GET artciles
|
|
173
|
+
} else if (req.method === "GET" && req.url === "/") {
|
|
174
|
+
// load articles
|
|
175
|
+
|
|
176
|
+
const html = await this.toHTML(); // render this blog to HTML
|
|
93
177
|
res.writeHead(200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
94
178
|
res.end(html);
|
|
179
|
+
} else {
|
|
180
|
+
// Error 404
|
|
181
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
182
|
+
res.end(JSON.stringify({ message: "Not Found" }));
|
|
95
183
|
}
|
|
96
184
|
});
|
|
97
185
|
|
|
98
|
-
server
|
|
99
|
-
|
|
186
|
+
this.#server = server;
|
|
187
|
+
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
this.#server.listen(port, () => {
|
|
190
|
+
console.log(`Server running at http://localhost:${port}/`);
|
|
191
|
+
resolve(); // Resolve the promise when the server is listening
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
} // http server
|
|
195
|
+
|
|
196
|
+
async closeServer() {
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
if (this.#server) {
|
|
199
|
+
this.#server.close((err) => {
|
|
200
|
+
if (err) return reject(err);
|
|
201
|
+
console.log("Server closed.");
|
|
202
|
+
resolve();
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
resolve(); // Nothing to close
|
|
206
|
+
}
|
|
100
207
|
});
|
|
101
208
|
}
|
|
102
209
|
|
|
210
|
+
/** save blog content to file */
|
|
103
211
|
async save(filename = this.filename) {
|
|
104
|
-
if (this
|
|
212
|
+
if (this.#apiServer === undefined) this.init(); // init blog if it didn't already happen
|
|
213
|
+
//await this.#apiServer.initialize();
|
|
214
|
+
if (this.#isExternalAPI) await this.loadFromAPI(this.#APIUrl);
|
|
105
215
|
if (!filename) {
|
|
106
216
|
console.error("Error: Filename not provided and not set previously.");
|
|
107
217
|
return;
|
|
@@ -120,6 +230,7 @@ export default class Blog {
|
|
|
120
230
|
}
|
|
121
231
|
}
|
|
122
232
|
|
|
233
|
+
/** load blog content from file */
|
|
123
234
|
async load(filename) {
|
|
124
235
|
this.filename = filename;
|
|
125
236
|
const data = await fs.readFile(filename, "utf8");
|
|
@@ -130,23 +241,28 @@ export default class Blog {
|
|
|
130
241
|
);
|
|
131
242
|
}
|
|
132
243
|
|
|
244
|
+
/** Populates the blog's title and articles from a data object. */
|
|
245
|
+
#loadFromData(data) {
|
|
246
|
+
this.articles = []; // Clear existing articles before loading new ones
|
|
247
|
+
this.setTitle(data.title);
|
|
248
|
+
// Assuming the API returns an array of objects with title and content
|
|
249
|
+
if (data.articles && Array.isArray(data.articles)) {
|
|
250
|
+
for (const articleData of data.articles) {
|
|
251
|
+
this.addArticle(new Article(articleData.title, articleData.content));
|
|
252
|
+
}
|
|
253
|
+
this.#apiDataLoaded = true; // Mark that we have successfully loaded data
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
133
257
|
async loadFromAPI(apiUrl) {
|
|
134
|
-
const data = await this
|
|
258
|
+
const data = await this.#fetchDataFromApi(apiUrl);
|
|
135
259
|
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
|
-
}
|
|
260
|
+
this.#loadFromData(data);
|
|
145
261
|
}
|
|
146
262
|
}
|
|
147
263
|
|
|
264
|
+
/** print markdown to the console */
|
|
148
265
|
async print() {
|
|
149
|
-
if (this.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
|
|
150
266
|
console.log(`# ${this.title}`);
|
|
151
267
|
for (const article of this.articles) {
|
|
152
268
|
console.log(`## ${article.title}`);
|
|
@@ -154,10 +270,11 @@ export default class Blog {
|
|
|
154
270
|
}
|
|
155
271
|
}
|
|
156
272
|
|
|
273
|
+
/** render this blog content to html */
|
|
157
274
|
async toHTML() {
|
|
158
275
|
// If we have an API URL and haven't loaded data yet, load it now.
|
|
159
|
-
if (this
|
|
160
|
-
await this.loadFromAPI(this
|
|
276
|
+
if (this.#APIUrl && !this.#apiDataLoaded) {
|
|
277
|
+
await this.loadFromAPI(this.#APIUrl);
|
|
161
278
|
}
|
|
162
279
|
|
|
163
280
|
return `<!DOCTYPE html>
|
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/index.js
CHANGED
package/model.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Sequelize, DataTypes } from "sequelize";
|
|
2
|
+
|
|
3
|
+
export default class ApiServer {
|
|
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: "./blog.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
|
+
}
|
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 += " ";
|
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
|
});
|
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;
|