@lexho111/plainblog 0.0.6 → 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/.eslintignore ADDED
File without changes
package/.eslintrc.json ADDED
File without changes
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
- async fetchDataFromApi(apiUrl) {
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
- async postDataToApi(apiUrl, postData) {
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
- setTitle(title) {
43
- this.title = title;
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
- addArticle(article) {
47
- this.articles.push(article);
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
- setStyle(style) {
51
- this.style = style;
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
- APIUrl = "";
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
- setAPI(APIUrl) {
57
- this.APIUrl = APIUrl;
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.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
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
- 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();
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.listen(port, () => {
99
- console.log(`Server running at http://localhost:${port}/`);
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.APIUrl.length > 0) await this.loadFromAPI(this.APIUrl);
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.fetchDataFromApi(apiUrl);
258
+ const data = await this.#fetchDataFromApi(apiUrl);
135
259
  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
- }
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.APIUrl && !this.apiDataLoaded) {
160
- await this.loadFromAPI(this.APIUrl);
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
- ## 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,45 +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";
53
- await storageserver("postgres", 8081); // you can use a postgres db too
54
+ import Blog from "@lexho111/plainblog";
55
+ import { Article } from "@lexho111/plainblog";
56
+
54
57
  const blog = new Blog();
55
- blog.setAPI(storageserver.getAPIURL());
56
58
  blog.setStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }");
57
59
 
58
- blog.startServer(8080);
59
- ```
60
+ const article = new Article("hello", "hello world!");
61
+ blog.addArticle(article);
60
62
 
61
- save data to file
62
-
63
- ```
64
- # save your blog to 'myblog.json'
65
- await blog.save("myblog.json");
63
+ blog.save("blog.json");
66
64
 
67
65
  # load data from 'myblog.json'
68
66
  await blog.load("myblog.json");
package/blog.db ADDED
Binary file
@@ -0,0 +1,45 @@
1
+ import globals from "globals";
2
+ import pluginJs from "@eslint/js";
3
+ import pluginJest from "eslint-plugin-jest";
4
+
5
+ export default [
6
+ {
7
+ // Configuration for all JavaScript files
8
+ files: ["**/*.js"],
9
+ languageOptions: {
10
+ ecmaVersion: 2022, // Supports modern JavaScript features
11
+ sourceType: "module",
12
+ globals: {
13
+ ...globals.node, // Defines Node.js global variables (e.g., `process`, `require`)
14
+ },
15
+ },
16
+ // ESLint's recommended rules for general JavaScript
17
+ rules: {
18
+ ...pluginJs.configs.recommended.rules,
19
+ // Add or override general JavaScript rules here.
20
+ // Example: Enforce semicolons at the end of statements
21
+ semi: ["error", "always"],
22
+ // Example: Prefer `const` over `let` where variables are not reassigned
23
+ "prefer-const": "error",
24
+ // Example: Prevent unused variables (can be configured further)
25
+ "no-unused-vars": ["warn", { args: "none" }],
26
+ },
27
+ },
28
+ {
29
+ // Configuration specifically for Jest test files
30
+ files: ["**/*.test.js", "**/*.spec.js"],
31
+ languageOptions: {
32
+ globals: {
33
+ ...globals.jest, // Defines Jest global variables (e.g., `describe`, `it`, `expect`)
34
+ },
35
+ },
36
+ plugins: {
37
+ jest: pluginJest,
38
+ },
39
+ // Recommended Jest rules from `eslint-plugin-jest`
40
+ rules: {
41
+ ...pluginJest.configs.recommended.rules,
42
+ // Add or override Jest-specific rules here.
43
+ },
44
+ },
45
+ ];
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;
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
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.0.6",
3
+ "version": "0.0.9",
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
  "scripts": {
8
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
8
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
9
+ "lint": "eslint ."
9
10
  },
10
11
  "keywords": [
11
12
  "blog",
@@ -22,6 +23,8 @@
22
23
  "sqlite3": "^5.1.7"
23
24
  },
24
25
  "devDependencies": {
25
- "jest": "^30.2.0"
26
+ "eslint": "^9.8.0",
27
+ "eslint-plugin-jest": "^28.6.0",
28
+ "jest": "^29.7.0"
26
29
  }
27
30
  }
@@ -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 += " ";
@@ -1,27 +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(done);
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();
16
34
  });
17
35
 
18
- test("should respond with 200 OK on the /health endpoint", async () => {
19
- 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
+ };
20
41
 
21
- // Check for a successful status code
22
- 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
+ });
23
47
 
24
- const body = await response.json();
25
- expect(body).toEqual({ status: "ok" });
48
+ expect(response.status).toBe(201);
49
+ const responseData = await response.json();
50
+ expect(responseData).toEqual(newArticle);
26
51
  });
27
52
  });
package/api-server.js DELETED
@@ -1,162 +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
- let dbport = 5432;
25
-
26
- export function setUsername(username1) {
27
- username = username1;
28
- }
29
- export function setPassword(password1) {
30
- password = password1;
31
- }
32
- export function setHost(host1) {
33
- host = host1;
34
- }
35
-
36
- let serverPort = 8081; // Default port
37
-
38
- export async function start(databasetype, port = 8081) {
39
- let sequelize;
40
- serverPort = port; // Update the port for getAPIURL
41
-
42
- if (databasetype === "sqlite") {
43
- sequelize = new Sequelize({
44
- dialect: "sqlite",
45
- storage: "./blog.db",
46
- logging: false,
47
- });
48
- } else if (databasetype === "postgres") {
49
- if (!username || !password || !host) {
50
- throw new Error(
51
- "PostgreSQL credentials not set. Please use setUsername(), setPassword(), and setHost() before starting the server."
52
- );
53
- }
54
- sequelize = new Sequelize(
55
- `postgres://${username}:${password}@${host}:${dbport}/blog`,
56
- {
57
- logging: false,
58
- }
59
- ); // Connect to local Docker container
60
- } else {
61
- console.error(
62
- `Invalid or no database type specified. Received: "${databasetype}". Exiting.`
63
- );
64
- process.exit(1);
65
- }
66
-
67
- // Define the Article model using the single sequelize instance
68
- const Article = sequelize.define(
69
- "Article",
70
- {
71
- title: DataTypes.STRING,
72
- content: DataTypes.STRING,
73
- },
74
- {
75
- timestamps: false, // Assuming you don't need createdAt/updatedAt for this simple model
76
- }
77
- );
78
-
79
- try {
80
- // Sync all models with the database (creates tables if they don't exist)
81
- await sequelize.sync();
82
- console.log("Database synchronized.");
83
- // Optionally, populate initial data if the database is empty
84
- if ((await Article.count()) === 0) {
85
- for (const articleData of blogData.articles) {
86
- await Article.create(articleData);
87
- }
88
- console.log("Initial blog data populated.");
89
- }
90
-
91
- const server = http.createServer(async (req, res) => {
92
- // Set CORS headers for all responses
93
- res.setHeader("Access-Control-Allow-Origin", "*");
94
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
95
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
96
-
97
- // Handle preflight CORS requests
98
- if (req.method === "OPTIONS") {
99
- res.writeHead(204);
100
- res.end();
101
- return;
102
- }
103
-
104
- // Health check endpoint
105
- if (req.method === "GET" && req.url === "/health") {
106
- res.writeHead(200, { "Content-Type": "application/json" });
107
- res.end(JSON.stringify({ status: "ok" }));
108
- return;
109
- }
110
-
111
- // GET all blog data
112
- if (req.method === "GET" && req.url === "/blog") {
113
- res.writeHead(200, { "Content-Type": "application/json" });
114
- const dbArticles = await Article.findAll();
115
- const responseData = {
116
- title: blogData.title, // Keep the title from the original constant
117
- articles: dbArticles,
118
- };
119
- res.end(JSON.stringify(responseData));
120
- }
121
- // POST a new article
122
- else if (req.method === "POST" && req.url === "/blog/articles") {
123
- let body = "";
124
- req.on("data", (chunk) => (body += chunk.toString()));
125
- req.on("end", async () => {
126
- const newArticle = JSON.parse(body);
127
- await Article.create(newArticle);
128
- console.log("Added new article:", newArticle);
129
- res.writeHead(201, { "Content-Type": "application/json" });
130
- res.end(JSON.stringify(newArticle));
131
- });
132
- } else {
133
- res.writeHead(404, { "Content-Type": "application/json" });
134
- res.end(JSON.stringify({ message: "Not Found" }));
135
- }
136
- });
137
-
138
- return new Promise((resolve) => {
139
- server.listen(port, () => {
140
- console.log(`API server running at http://localhost:${port}/`);
141
- resolve(server);
142
- });
143
- });
144
- } catch (error) {
145
- console.error("Failed to initialize database or start server:", error);
146
- process.exit(1); // Exit the process if there's a critical error
147
- }
148
- }
149
-
150
- function getAPIURL() {
151
- return `http://localhost:${serverPort}/blog`;
152
- }
153
-
154
- const storageserver = {
155
- start,
156
- getAPIURL,
157
- setUsername,
158
- setPassword,
159
- setHost,
160
- };
161
-
162
- export default storageserver;