@lexho111/plainblog 0.6.11 → 0.6.13

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.
@@ -0,0 +1 @@
1
+ {"nodes":[{"id":1,"callFrame":{"functionName":"(root)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":0,"children":[2]},{"id":2,"callFrame":{"functionName":"processTicksAndRejections","scriptId":"31","url":"node:internal/process/task_queues","lineNumber":70,"columnNumber":34},"hitCount":0,"children":[3]},{"id":3,"callFrame":{"functionName":"runSearch","scriptId":"165","url":"file:///C:/Users/alexh/Documents/plainblog/package/profile-bst.js","lineNumber":11,"columnNumber":24},"hitCount":2,"children":[4,6,7],"positionTicks":[{"line":38,"ticks":2}]},{"id":4,"callFrame":{"functionName":"","scriptId":"165","url":"file:///C:/Users/alexh/Documents/plainblog/package/profile-bst.js","lineNumber":31,"columnNumber":20},"hitCount":0,"children":[5]},{"id":5,"callFrame":{"functionName":"post","scriptId":"108","url":"node:inspector","lineNumber":114,"columnNumber":6},"hitCount":0},{"id":6,"callFrame":{"functionName":"getRange","scriptId":"167","url":"file:///C:/Users/alexh/Documents/plainblog/package/model/datastructures/ArrayList.js","lineNumber":64,"columnNumber":16},"hitCount":9,"positionTicks":[{"line":83,"ticks":4},{"line":83,"ticks":1},{"line":85,"ticks":3},{"line":83,"ticks":1}]},{"id":7,"callFrame":{"functionName":"","scriptId":"165","url":"file:///C:/Users/alexh/Documents/plainblog/package/profile-bst.js","lineNumber":39,"columnNumber":20},"hitCount":0,"children":[8]},{"id":8,"callFrame":{"functionName":"post","scriptId":"108","url":"node:inspector","lineNumber":114,"columnNumber":6},"hitCount":0,"children":[9]},{"id":9,"callFrame":{"functionName":"dispatch","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":1,"positionTicks":[{"line":139,"ticks":1}]}],"startTime":1715759778061,"endTime":1715763735963,"samples":[5,6,3,6,6,6,6,6,6,6,6,6,6,3,9],"timeDeltas":[3939197,1797,1542,1458,1498,1489,1511,506,1007,1507,1835,1295,853,497,1509]}
package/Article.js CHANGED
@@ -24,20 +24,41 @@ export default class Article {
24
24
  }
25
25
 
26
26
  getContentVeryShort() {
27
- if (!this.content) return;
28
- if (this.content.length < 50) return this.getContent();
27
+ if (!this.content) return "";
28
+ if (this.content.length < 50) return this.content;
29
29
  let contentShort = this.content.substring(0, 50);
30
30
  contentShort += "...";
31
31
  return contentShort;
32
32
  }
33
33
 
34
34
  getContentShort() {
35
- if (this.content.length < 400) return this.getContent();
35
+ if (!this.content) return "";
36
+ if (this.content.length < 400) return this.content;
36
37
  let contentShort = this.content.substring(0, 400);
37
38
  contentShort += "...";
38
39
  return contentShort;
39
40
  }
40
41
 
42
+ /**
43
+ * Returns a JSON-serializable representation of the article.
44
+ * This method is automatically called by JSON.stringify().
45
+ */
46
+ toJSON() {
47
+ // Return a plain object with the desired properties
48
+ let date = new Date();
49
+ try {
50
+ date = new Date(this.createdAt).toISOString();
51
+ } catch (err) {
52
+ console.error(err);
53
+ }
54
+ return {
55
+ id: this.id,
56
+ title: this.title,
57
+ content: this.content,
58
+ createdAt: date,
59
+ };
60
+ }
61
+
41
62
  getFormattedDate() {
42
63
  const date = new Date(this.createdAt);
43
64
  const year = date.getFullYear();
@@ -51,48 +72,8 @@ export default class Article {
51
72
  toHTML(loggedin = false) {
52
73
  if (this.content == null) return "";
53
74
  const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
54
- const buttons = loggedin
55
- ? `<div class="buttons"><div id="editButton${this.id}" class="btn edit">edit</div><div id="deleteButton${this.id}" class="btn delete">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
56
- : "";
57
- const script = loggedin
58
- ? `<script>
59
- if (typeof window.handleAction !== 'function') {
60
- window.handleAction = function(action, id) {
61
- if (action === 'delete') {
62
- if (confirm("Delete this article?")) {
63
- fetch("/api/articles?id=" + id, { method: "DELETE" })
64
- .then(res => { if(res.ok) window.location.reload(); else alert("Error: " + res.statusText); });
65
- }
66
- } else if (action === 'edit') {
67
- const title = prompt("New Title");
68
- const content = prompt("New Content");
69
- if (title && content) {
70
- fetch("/api/articles?id=" + id, {
71
- method: "PUT",
72
- body: JSON.stringify({ title, content })
73
- }).then(res => { if(res.ok) window.location.reload(); else alert("Error: " + res.statusText); });
74
- }
75
- }
76
- }
77
- }
78
- function clickListener(button) {
79
- const mybutton = document.getElementById(button);
80
- const action = button.includes("edit") ? "edit" : "delete";
81
-
82
- // Mouse/Touch support
83
- mybutton.addEventListener('click', () => window.handleAction(action, ${this.id}));
84
-
85
- // Keyboard support
86
- mybutton.addEventListener('keydown', (event) => {
87
- if (event.key === 'Enter' || event.key === ' ') {
88
- event.preventDefault(); // Prevents page scrolling on Space
89
- window.handleAction(action, ${this.id});
90
- }
91
- });
92
- }
93
- clickListener("editButton${this.id}");
94
- clickListener("deleteButton${this.id}");
95
- </script>`
75
+ const buttons = loggedin // prettier-ignore
76
+ ? `<div class="buttons"><div id="editButton${this.id}" class="btn edit" role="button" tabindex="0" data-action="edit" data-id="${this.id}">edit</div><div id="deleteButton${this.id}" class="btn delete" role="button" tabindex="0" data-action="delete" data-id="${this.id}">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
96
77
  : "";
97
78
  // prettier-ignore
98
79
  return `<article data-id="${this.id}" data-date="${this.createdAt}">
@@ -101,7 +82,6 @@ clickListener("deleteButton${this.id}");
101
82
  <span class="datetime">${this.getFormattedDate()}</span>
102
83
  <p>${this.getContentShort()}</p>
103
84
  ${moreButton}
104
- ${script}
105
85
  </article>`;
106
86
  }
107
87
  }
@@ -0,0 +1 @@
1
+ {"nodes":[{"id":1,"callFrame":{"functionName":"(root)","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":0,"children":[2]},{"id":2,"callFrame":{"functionName":"processTicksAndRejections","scriptId":"31","url":"node:internal/process/task_queues","lineNumber":70,"columnNumber":34},"hitCount":0,"children":[3]},{"id":3,"callFrame":{"functionName":"runSearch","scriptId":"165","url":"file:///C:/Users/alexh/Documents/plainblog/package/profile-bst.js","lineNumber":11,"columnNumber":24},"hitCount":0,"children":[4,6]},{"id":4,"callFrame":{"functionName":"","scriptId":"165","url":"file:///C:/Users/alexh/Documents/plainblog/package/profile-bst.js","lineNumber":31,"columnNumber":20},"hitCount":0,"children":[5]},{"id":5,"callFrame":{"functionName":"post","scriptId":"108","url":"node:inspector","lineNumber":114,"columnNumber":6},"hitCount":0},{"id":6,"callFrame":{"functionName":"","scriptId":"165","url":"file:///C:/Users/alexh/Documents/plainblog/package/profile-bst.js","lineNumber":39,"columnNumber":20},"hitCount":0,"children":[7]},{"id":7,"callFrame":{"functionName":"post","scriptId":"108","url":"node:inspector","lineNumber":114,"columnNumber":6},"hitCount":0,"children":[8]},{"id":8,"callFrame":{"functionName":"dispatch","scriptId":"0","url":"","lineNumber":-1,"columnNumber":-1},"hitCount":1,"positionTicks":[{"line":139,"ticks":1}]}],"startTime":1715784018183,"endTime":1715790062379,"samples":[5,8],"timeDeltas":[6035769,7757]}
package/Blog.js CHANGED
@@ -6,6 +6,7 @@ import process from "node:process";
6
6
  import path from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import pkg from "./package.json" with { type: "json" };
9
+ import { Buffer } from "node:buffer";
9
10
  import Article from "./Article.js";
10
11
  import DatabaseModel from "./model/DatabaseModel.js";
11
12
  import { fetchData, postData } from "./model/APIModel.js";
@@ -56,7 +57,7 @@ export default class Blog {
56
57
  }
57
58
 
58
59
  /** @returns a json representation of the blog */
59
- json() {
60
+ toJSON() {
60
61
  const serverInfo = this.#server
61
62
  ? {
62
63
  listening: this.#server.listening,
@@ -80,6 +81,11 @@ export default class Blog {
80
81
  return JSON.parse(JSON.stringify(json)); // make json read-only
81
82
  }
82
83
 
84
+ /** @returns a json representation of the blog */
85
+ json() {
86
+ return this.toJSON();
87
+ }
88
+
83
89
  // Private fields
84
90
  #version = null;
85
91
  #server = null;
@@ -171,19 +177,29 @@ export default class Blog {
171
177
 
172
178
  async #readBody(req) {
173
179
  return new Promise((resolve, reject) => {
174
- let data = "";
175
- req.on("data", (chunk) => (data += chunk.toString()));
176
- req.on("end", () => resolve(data));
177
- req.on("error", reject);
180
+ const chunks = [];
181
+
182
+ req.on("data", (chunk) => {
183
+ chunks.push(chunk);
184
+ });
185
+
186
+ req.on("end", () => {
187
+ const data = Buffer.concat(chunks).toString();
188
+ resolve(data);
189
+ });
190
+
191
+ req.on("error", (err) => {
192
+ reject(err);
193
+ });
178
194
  });
179
195
  }
180
196
 
181
197
  async #handleLogin(req, res) {
182
198
  debug("handle login");
183
199
  const body = await new Promise((resolve, reject) => {
184
- let data = "";
185
- req.on("data", (chunk) => (data += chunk.toString()));
186
- req.on("end", () => resolve(data));
200
+ const chunks = [];
201
+ req.on("data", (chunk) => chunks.push(chunk));
202
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
187
203
  req.on("error", reject);
188
204
  });
189
205
  const params = new URLSearchParams(body);
@@ -263,7 +279,9 @@ export default class Blog {
263
279
  );
264
280
  if (match) publicHash = match[1];
265
281
  })
266
- .catch((err) => console.error(err)), // public/styles.min.css doesn't exist, will be created.
282
+ .catch((err) => {
283
+ if (err.code !== "ENOENT") console.error(err);
284
+ }), // public/styles.min.css doesn't exist, will be created.
267
285
  fs.promises
268
286
  .readFile(srcStylePath, "utf8")
269
287
  .then((content) => {
@@ -307,14 +325,14 @@ export default class Blog {
307
325
  console.log("external API");
308
326
  await this.#loadFromAPI();
309
327
  } else {
310
- console.log(`database: ${this.database.type}`);
328
+ debug(`database: ${this.database.type}`);
311
329
  if (!this.#databaseModel) {
312
330
  if (this.database.type === "file") {
313
331
  const adapter = new FileAdapter(this.database);
314
332
  this.#databaseModel = new DatabaseModel(adapter);
315
333
  }
316
334
  }
317
- console.log(`connected to database`);
335
+ debug(`connected to database`);
318
336
  await this.#databaseModel.initialize();
319
337
  this.#articles.setDatabase(this.#databaseModel);
320
338
  const [dbTitle, dbArticles] = await Promise.all([
@@ -364,16 +382,21 @@ export default class Blog {
364
382
  async postArticle(newArticle) {
365
383
  try {
366
384
  // Save the new article to the database via the ApiServer
367
- const promises = [];
368
- //if (this.#databaseModel)
369
- // promises.push(this.#databaseModel.save(newArticle));
370
- if (this.#isExternalAPI)
371
- promises.push(postData(this.#apiUrl, newArticle));
372
- await Promise.all(promises);
373
- const title = newArticle.title;
374
- const content = newArticle.content;
385
+ if (this.#isExternalAPI) {
386
+ await postData(this.#apiUrl, newArticle);
387
+ }
388
+
389
+ // The article is already saved to the DB, just add it to the in-memory cache.
390
+ // Ensure it's an Article instance.
391
+ const article = new Article(
392
+ newArticle.id,
393
+ newArticle.title,
394
+ newArticle.content,
395
+ newArticle.createdAt,
396
+ );
397
+
375
398
  // Add the article to the local list for immediate display
376
- this.#articles.insert(Article.createNew(title, content));
399
+ this.#articles.insert(article);
377
400
  // remove sample entries
378
401
  this.#articles.remove(1); // "Sample Entry #1"
379
402
  this.#articles.remove(2); // "Sample Entry #2"
@@ -471,8 +494,8 @@ export default class Blog {
471
494
 
472
495
  console.log("New Article Data:", articleData);
473
496
  // local
474
- await this.#databaseModel.save(articleData); // --> to api server
475
- this.postArticle(articleData);
497
+ const savedArticle = await this.#databaseModel.save(articleData);
498
+ this.postArticle(savedArticle);
476
499
  // external
477
500
 
478
501
  // Success response
@@ -696,7 +719,10 @@ export default class Blog {
696
719
  //console.log(this.#articles.getAllArticles());
697
720
  const article = this.#articles.get(id);
698
721
  if (article) {
699
- res.writeHead(200, { "Content-Type": "application/json" });
722
+ res.writeHead(200, {
723
+ "Content-Type": "application/json",
724
+ "Cache-Control": "public, max-age=60", // Cache for 60 seconds
725
+ });
700
726
  res.end(JSON.stringify(article));
701
727
  } else {
702
728
  res.writeHead(404, { "Content-Type": "application/json" });
@@ -742,19 +768,27 @@ export default class Blog {
742
768
  return;
743
769
  }
744
770
  const body = await new Promise((resolve, reject) => {
745
- let data = "";
746
- req.on("data", (chunk) => (data += chunk.toString()));
747
- req.on("end", () => resolve(data));
771
+ const chunks = [];
772
+
773
+ req.on("data", (chunk) => {
774
+ chunks.push(chunk);
775
+ });
776
+
777
+ req.on("end", () => {
778
+ const result = Buffer.concat(chunks).toString();
779
+ resolve(result);
780
+ });
781
+
748
782
  req.on("error", reject);
749
783
  });
750
784
  const newArticle = JSON.parse(body);
751
785
  debug("new article: %s", newArticle.title);
752
786
  // local
753
- await this.#databaseModel.save(newArticle); // --> to api server
754
- this.postArticle(newArticle);
787
+ const savedArticle = await this.#databaseModel.save(newArticle);
788
+ this.postArticle(savedArticle);
755
789
  // external
756
790
  res.writeHead(201, { "Content-Type": "application/json" });
757
- res.end(JSON.stringify(newArticle));
791
+ res.end(JSON.stringify(savedArticle));
758
792
  } else if (req.method === "DELETE") {
759
793
  debug("DELETE an article");
760
794
  const match = pathname.match(/^\/api\/articles\/(\d+)$/);
@@ -786,10 +820,20 @@ export default class Blog {
786
820
  if (pathname === "/api/articles" || match) {
787
821
  const id = match ? match[1] : url.searchParams.get("id");
788
822
  debug("PUT article id: %d", id);
789
- const body = await new Promise((resolve) => {
790
- let data = "";
791
- req.on("data", (chunk) => (data += chunk));
792
- req.on("end", () => resolve(data));
823
+ const body = await new Promise((resolve, reject) => {
824
+ const chunks = [];
825
+
826
+ req.on("data", (chunk) => {
827
+ // Speichert die Buffer-Referenzen direkt, ohne String-Konvertierung
828
+ chunks.push(chunk);
829
+ });
830
+
831
+ req.on("end", () => {
832
+ const data = Buffer.concat(chunks).toString();
833
+ resolve(data);
834
+ });
835
+
836
+ req.on("error", reject);
793
837
  });
794
838
  const { title, content } = JSON.parse(body);
795
839
  this.#articles.update(parseInt(id), title, content);
@@ -820,10 +864,9 @@ export default class Blog {
820
864
 
821
865
  /** render this blog content to valid html */
822
866
  async toHTML(loggedin) {
823
- const articles = this.#articles.getAllArticles();
824
- const articles_after = articles.slice(0, 50);
867
+ const articles_after = await this.#articles.findAll(null, null, 50);
825
868
  // prettier-ignore
826
- debug("slice articles from %d to %d", articles.length, articles_after.length);
869
+ debug("fetched %d articles", articles_after.length);
827
870
  const data = {
828
871
  title: this.title,
829
872
  articles: articles_after,
@@ -843,13 +886,15 @@ export default class Blog {
843
886
  //debug("typeof data.articles: %o", typeof data.articles);
844
887
  //debug("typeof data.articles: %O", data.articles);
845
888
  const article = data.articles[0];
846
- debug("first article: ");
847
- debug(
848
- "%d %s %s...",
849
- article.id,
850
- article.title,
851
- article.getContentVeryShort(),
852
- );
889
+ if (article) {
890
+ debug("first article: ");
891
+ debug(
892
+ "%d %s %s...",
893
+ article.id,
894
+ article.title,
895
+ article.getContentVeryShort(),
896
+ );
897
+ }
853
898
  const html = formatHTML(data);
854
899
  if (validate(html)) return html;
855
900
  throw new Error("Error. Invalid HTML!");
@@ -860,7 +905,7 @@ export default class Blog {
860
905
  * @param {string[]} files - Array of css/scss file paths to process.
861
906
  */
862
907
  async #processStylesheets(files) {
863
- console.log("process stylesheets");
908
+ debug("process stylesheets");
864
909
 
865
910
  // Normalize input to array (handles string or array)
866
911
  // "file1.css" --> ["file1.css"]
@@ -886,16 +931,11 @@ export default class Blog {
886
931
  );
887
932
 
888
933
  // compute hash
889
- const currentHash = crypto
890
- .createHash("sha256")
891
- .update(
892
- fileData
893
- .map((f) =>
894
- crypto.createHash("sha256").update(f.content).digest("hex"),
895
- )
896
- .join(""),
897
- )
898
- .digest("hex");
934
+ const hash = crypto.createHash("sha256");
935
+ for (const file of fileData) {
936
+ hash.update(file.content);
937
+ }
938
+ const currentHash = hash.digest("hex");
899
939
 
900
940
  // check if hash matches
901
941
  if (currentHash !== this.#stylesHash && this.compilestyle) {
@@ -938,16 +978,11 @@ export default class Blog {
938
978
  }),
939
979
  );
940
980
 
941
- const currentHash = crypto
942
- .createHash("sha256")
943
- .update(
944
- fileData
945
- .map((f) =>
946
- crypto.createHash("sha256").update(f.content).digest("hex"),
947
- )
948
- .join(""),
949
- )
950
- .digest("hex");
981
+ const hash = crypto.createHash("sha256");
982
+ for (const file of fileData) {
983
+ hash.update(file.content);
984
+ }
985
+ const currentHash = hash.digest("hex");
951
986
 
952
987
  if (!this.#scriptsHash) {
953
988
  try {