@lexho111/plainblog 0.8.5 → 0.9.0

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 CHANGED
@@ -273,7 +273,7 @@ export default class Blog {
273
273
  this.#articles.setDatabase(this.#databaseModel);
274
274
  const [dbTitle, dbArticles] = await Promise.all([
275
275
  this.#databaseModel.getBlogTitle(),
276
- this.#databaseModel.findAll(-1),
276
+ this.#databaseModel.findAll(-1), // dbArticles should be ordered by date (BST)
277
277
  ]);
278
278
  debug("dbArticles.size(): %d", dbArticles.length);
279
279
  //log(filename, "dbArticles.size(): " + dbArticles.length);
@@ -405,6 +405,7 @@ export default class Blog {
405
405
  }
406
406
  this.#title = data.title;
407
407
  // Assuming data contains a title and an array of articles with title and content
408
+ // data.articles should be ordered by date (BST)
408
409
  if (this.#articles && data.articles && Array.isArray(data.articles)) {
409
410
  if (this.#articles.getStorageName().includes("BinarySearchTree")) {
410
411
  debug("using special insert method for BST");
@@ -466,8 +467,30 @@ export default class Blog {
466
467
  /** render this blog content to valid html */
467
468
  async toHTML(loggedin, params = {}) {
468
469
  let articles = [];
470
+ let singleArticle = false;
469
471
  if (params.q) {
470
472
  articles = this.#articles.search(params.q);
473
+ } else if (params.id) {
474
+ const id = parseInt(params.id);
475
+ let article = this.#articles.get(id);
476
+ if (!article && this.#databaseModel) {
477
+ const dbResults = await this.#databaseModel.findAll(1, 0, id, id);
478
+ if (dbResults.length > 0) {
479
+ const data = dbResults[0];
480
+ article = new Article(
481
+ data.id,
482
+ data.title,
483
+ data.content,
484
+ data.createdAt,
485
+ data.image,
486
+ );
487
+ this.#articles.insert(article);
488
+ }
489
+ }
490
+ if (article) {
491
+ articles = [article];
492
+ singleArticle = true;
493
+ }
471
494
  } else if (params.date) {
472
495
  const d = new Date(params.date);
473
496
  if (!isNaN(d.getTime())) {
@@ -491,6 +514,7 @@ export default class Blog {
491
514
  articles: articles,
492
515
  latestArticleDate: latestArticle ? latestArticle.createdAt : null,
493
516
  loggedin,
517
+ singleArticle,
494
518
  login: "",
495
519
  };
496
520
 
package/Formatter.js CHANGED
@@ -16,6 +16,7 @@ const articleTemplate = loadTemplate("article.html");
16
16
  const articleButtonsTemplate = loadTemplate("articleButtons.html");
17
17
  const moreButtonTemplate = loadTemplate("moreButton.html");
18
18
  const timelineTemplate = loadTemplate("timelineSelector.html");
19
+ const loginTemplate = loadTemplate("login.html");
19
20
 
20
21
  /**
21
22
  * generates the header of the generated html file
@@ -30,7 +31,7 @@ function createNew() {
30
31
  return formTemplate;
31
32
  }
32
33
 
33
- function formatArticle(article, loggedin) {
34
+ function formatArticle(article, loggedin, showFull = false) {
34
35
  if (article.content == null) return "";
35
36
 
36
37
  let buttons = "";
@@ -38,22 +39,33 @@ function formatArticle(article, loggedin) {
38
39
  buttons = articleButtonsTemplate.replaceAll("${id}", article.id);
39
40
  }
40
41
 
41
- const moreButton = article.content.length > 400 ? moreButtonTemplate : "";
42
+ const moreButton =
43
+ !showFull && article.content.length > 400
44
+ ? moreButtonTemplate.replaceAll("${id}", article.id)
45
+ : "";
42
46
 
43
47
  /*const imageHtml = article.image
44
48
  ? `<img src="${article.image}" alt="${article.title}" style="max-width: 100%;">`
45
49
  : "";*/
46
50
 
47
51
  let html = articleTemplate;
48
- html = html.replace("${id}", article.id);
52
+ html = html.replaceAll("${id}", article.id);
49
53
  html = html.replace("${createdAt}", article.createdAt);
50
54
  html = html.replace("${buttons}", buttons);
51
55
  html = html.replace("${title}", article.title);
52
56
  html = html.replace("${formattedDate}", article.getFormattedDate());
53
57
  html = html.replace("${image}", article.image);
54
- html = html.replace("${contentShort}", article.getContentShort());
58
+ html = html.replace(
59
+ "${contentShort}",
60
+ showFull ? "" : article.getContentShort(),
61
+ );
55
62
  html = html.replace("${content}", article.content);
56
63
  html = html.replace("${moreButton}", moreButton);
64
+
65
+ if (showFull) {
66
+ html = html.replace('style="display: none;"', "");
67
+ }
68
+
57
69
  return html;
58
70
  }
59
71
 
@@ -97,18 +109,32 @@ function formatTimeline(latestArticleDate) {
97
109
  export function formatHTML(data) {
98
110
  const form = data.loggedin ? createNew() : "";
99
111
  const articles = data.articles
100
- .map((article) => formatArticle(article, data.loggedin))
112
+ .map((article) => formatArticle(article, data.loggedin, data.singleArticle))
101
113
  .join("");
102
- const timeline = formatTimeline(data.latestArticleDate);
114
+ const timeline = data.singleArticle
115
+ ? '<a href="/" class="btn backtohome">Back to Home</a>'
116
+ : formatTimeline(data.latestArticleDate);
103
117
  let html = layoutTemplate;
104
118
  html = html.replace("${login}", data.login);
105
- html = html.replace("${title}", data.title);
119
+ html = html.replace(
120
+ "${title}",
121
+ `<a href="/" style="text-decoration: none; color: inherit;">${data.title}</a>`,
122
+ );
106
123
  html = html.replace("${form}", form);
107
124
  html = html.replace("${timeline}", timeline);
108
125
  html = html.replace("${articles}", articles);
109
126
  return header(data.title) + "\n" + html;
110
127
  }
111
128
 
129
+ export function formatLogin(title) {
130
+ let html = loginTemplate;
131
+ html = html.replace(
132
+ "${title}",
133
+ `<a href="/" style="text-decoration: none; color: inherit;">${title}</a>`,
134
+ );
135
+ return header(title) + "\n" + html;
136
+ }
137
+
112
138
  /**
113
139
  * format content like articles to markdown
114
140
  * @param {*} data blog data like blogtitle and articles
@@ -0,0 +1,248 @@
1
+ //import { createDebug } from "../../debug-loader.js";
2
+ const createDebug = (namespace) => {
3
+ return (...args) => { };
4
+ };
5
+ const debug = createDebug("plainblog:PostgresAdapter");
6
+ export default class PostgresAdapter {
7
+ dbtype = "postgres";
8
+ username;
9
+ password;
10
+ host;
11
+ dbname;
12
+ dbport;
13
+ pool;
14
+ ready;
15
+ constructor(options = {}) {
16
+ debug(JSON.stringify(options));
17
+ this.username = options.username;
18
+ this.password = options.password;
19
+ this.host = options.host;
20
+ this.dbname = options.dbname || "blog";
21
+ this.dbport = options.dbport || 5432;
22
+ if (!this.username || !this.password || !this.host) {
23
+ throw new Error("PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options.");
24
+ }
25
+ this.ready = false;
26
+ this.pool = null;
27
+ }
28
+ async initialize() {
29
+ let pg;
30
+ try {
31
+ // @ts-ignore
32
+ pg = await import("pg");
33
+ }
34
+ catch (error) {
35
+ if (error.code === "ERR_MODULE_NOT_FOUND" ||
36
+ error.code === "MODULE_NOT_FOUND") {
37
+ console.error("The 'pg' package is not installed. Please install it by running: npm install pg");
38
+ process.exit(1);
39
+ }
40
+ else {
41
+ throw error;
42
+ }
43
+ }
44
+ const { Pool, Client } = pg.default || pg;
45
+ try {
46
+ const client = new Client({
47
+ user: this.username,
48
+ host: this.host,
49
+ database: "postgres",
50
+ password: this.password,
51
+ port: this.dbport,
52
+ });
53
+ await client.connect();
54
+ const res = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [this.dbname]);
55
+ if (res.rowCount === 0) {
56
+ await client.query(`CREATE DATABASE "${this.dbname}"`);
57
+ }
58
+ await client.end();
59
+ }
60
+ catch (e) {
61
+ console.error("Failed to check/create database:", e);
62
+ }
63
+ this.pool = new Pool({
64
+ user: this.username,
65
+ host: this.host,
66
+ database: this.dbname,
67
+ password: this.password,
68
+ port: this.dbport,
69
+ });
70
+ try {
71
+ const client = await this.pool.connect();
72
+ try {
73
+ await client.query(`
74
+ CREATE TABLE IF NOT EXISTS "Articles" (
75
+ id BIGSERIAL PRIMARY KEY,
76
+ title VARCHAR(255),
77
+ content TEXT,
78
+ image TEXT,
79
+ "createdAt" TIMESTAMP,
80
+ "updatedAt" TIMESTAMP
81
+ )
82
+ `);
83
+ await client.query(`
84
+ CREATE TABLE IF NOT EXISTS "BlogInfos" (
85
+ id SERIAL PRIMARY KEY,
86
+ title VARCHAR(255)
87
+ )
88
+ `);
89
+ const res = await client.query('SELECT count(*) as count FROM "BlogInfos"');
90
+ if (parseInt(res.rows[0].count) === 0) {
91
+ await client.query('INSERT INTO "BlogInfos" (title) VALUES ($1)', ["My Default Blog Title"]);
92
+ }
93
+ this.ready = true;
94
+ }
95
+ finally {
96
+ client.release();
97
+ }
98
+ }
99
+ catch (err) {
100
+ console.error("Failed to initialize PostgresAdapter", err);
101
+ throw err;
102
+ }
103
+ }
104
+ async terminate() {
105
+ if (this.pool) {
106
+ await this.pool.end();
107
+ this.pool = null;
108
+ this.ready = false;
109
+ }
110
+ }
111
+ getType() {
112
+ return this.dbtype;
113
+ }
114
+ getDBName() {
115
+ return this.dbname;
116
+ }
117
+ isReady() {
118
+ return this.ready;
119
+ }
120
+ async getBlogTitle() {
121
+ if (!this.pool)
122
+ await this.initialize();
123
+ const res = await this.pool.query('SELECT title FROM "BlogInfos" LIMIT 1');
124
+ return res.rows.length > 0 ? res.rows[0].title : "Blog";
125
+ }
126
+ async updateBlogTitle(newTitle) {
127
+ if (!this.pool)
128
+ await this.initialize();
129
+ await this.pool.query('UPDATE "BlogInfos" SET title = $1', [newTitle]);
130
+ }
131
+ async save(newArticle) {
132
+ if (!this.pool)
133
+ await this.initialize();
134
+ const createdAt = newArticle.createdAt
135
+ ? new Date(newArticle.createdAt)
136
+ : new Date();
137
+ const updatedAt = new Date();
138
+ const cols = ['title', 'content', 'image', '"createdAt"', '"updatedAt"'];
139
+ const vals = [
140
+ newArticle.title,
141
+ newArticle.content,
142
+ newArticle.image,
143
+ createdAt,
144
+ updatedAt,
145
+ ];
146
+ if (newArticle.id) {
147
+ cols.unshift("id");
148
+ vals.unshift(newArticle.id);
149
+ }
150
+ const finalPlaceholders = vals.map((_, i) => `$${i + 1}`);
151
+ const query = `
152
+ INSERT INTO "Articles" (${cols.join(", ")})
153
+ VALUES (${finalPlaceholders.join(", ")})
154
+ RETURNING id
155
+ `;
156
+ const res = await this.pool.query(query, vals);
157
+ const id = Number(res.rows[0].id);
158
+ return {
159
+ ...newArticle,
160
+ id,
161
+ createdAt,
162
+ updatedAt,
163
+ };
164
+ }
165
+ async update(id, data) {
166
+ if (!this.pool)
167
+ await this.initialize();
168
+ const sets = [];
169
+ const values = [];
170
+ let paramIndex = 1;
171
+ if (data.title !== undefined) {
172
+ sets.push(`title = $${paramIndex++}`);
173
+ values.push(data.title);
174
+ }
175
+ if (data.content !== undefined) {
176
+ sets.push(`content = $${paramIndex++}`);
177
+ values.push(data.content);
178
+ }
179
+ if (data.image !== undefined) {
180
+ sets.push(`image = $${paramIndex++}`);
181
+ values.push(data.image);
182
+ }
183
+ sets.push(`"updatedAt" = $${paramIndex++}`);
184
+ values.push(new Date());
185
+ if (sets.length > 0) {
186
+ values.push(id);
187
+ const query = `UPDATE "Articles" SET ${sets.join(", ")} WHERE id = $${paramIndex}`;
188
+ await this.pool.query(query, values);
189
+ }
190
+ }
191
+ async remove(id) {
192
+ if (!this.pool)
193
+ await this.initialize();
194
+ await this.pool.query('DELETE FROM "Articles" WHERE id = $1', [id]);
195
+ }
196
+ async findAll(limit = 4, offset = 0, startId = null, endId = null, order = "DESC") {
197
+ if (!this.pool)
198
+ await this.initialize();
199
+ let query = 'SELECT * FROM "Articles"';
200
+ const conditions = [];
201
+ const values = [];
202
+ let paramIndex = 1;
203
+ const isDate = typeof startId === "string" || typeof endId === "string";
204
+ if (startId !== null && endId !== null) {
205
+ if (isDate) {
206
+ conditions.push(`"createdAt" BETWEEN $${paramIndex++} AND $${paramIndex++}`);
207
+ values.push(startId, endId);
208
+ }
209
+ else {
210
+ conditions.push(`id BETWEEN $${paramIndex++} AND $${paramIndex++}`);
211
+ values.push(Math.min(Number(startId), Number(endId)), Math.max(Number(startId), Number(endId)));
212
+ }
213
+ }
214
+ else if (startId !== null) {
215
+ if (isDate) {
216
+ conditions.push(order === "DESC"
217
+ ? `"createdAt" <= $${paramIndex++}`
218
+ : `"createdAt" >= $${paramIndex++}`);
219
+ values.push(startId);
220
+ }
221
+ else {
222
+ conditions.push(order === "DESC" ? `id <= $${paramIndex++}` : `id >= $${paramIndex++}`);
223
+ values.push(startId);
224
+ }
225
+ }
226
+ if (conditions.length > 0) {
227
+ query += " WHERE " + conditions.join(" AND ");
228
+ }
229
+ query += ` ORDER BY "createdAt" ${order}, id ${order}`;
230
+ if (limit !== -1) {
231
+ query += ` LIMIT $${paramIndex++}`;
232
+ values.push(limit);
233
+ }
234
+ if (offset > 0) {
235
+ query += ` OFFSET $${paramIndex++}`;
236
+ values.push(offset);
237
+ }
238
+ const res = await this.pool.query(query, values);
239
+ return res.rows.map((row) => ({
240
+ id: Number(row.id),
241
+ title: row.title,
242
+ content: row.content,
243
+ image: row.image,
244
+ createdAt: new Date(row.createdAt),
245
+ updatedAt: new Date(row.updatedAt),
246
+ }));
247
+ }
248
+ }