@lexho111/plainblog 0.8.4 → 0.8.6

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/README.md CHANGED
@@ -28,14 +28,13 @@ or with the BlogBuilder
28
28
 
29
29
  ```
30
30
  import { BlogBuilder } from "@lexho111/plainblog";
31
+
31
32
  const blog = new BlogBuilder()
32
33
  .withTitle("Mein Blog")
33
34
  .withStyle("body { font-family: Arial, sans-serif; } h1 { color: #333; }")
34
35
  .withPassword("mypassword")
35
36
  .build();
36
-
37
37
  await blog.init();
38
-
39
38
  await blog.startServer({ httpPort: 38080, httpsPort: 38443 });
40
39
  ```
41
40
 
@@ -50,8 +49,10 @@ To add a headerphoto to your blog simply name it "headerphoto.jpg" and put it in
50
49
  You can put your own angular theme in the public folder.
51
50
 
52
51
  ```
52
+ const blog = new BlogBuilder()
53
53
  ...
54
- blog.angular = true;
54
+ .withFrontend("angular")
55
+ .build();
55
56
  ```
56
57
 
57
58
  ### set a Database Adapter
@@ -89,7 +90,7 @@ await blog.init();
89
90
  await blog.startServer();
90
91
  ```
91
92
 
92
- ### use differen ports
93
+ ### use different ports
93
94
 
94
95
  ```
95
96
  import Blog from "@lexho111/plainblog";
@@ -0,0 +1,262 @@
1
+ import { createDebug } from "../../debug-loader.js";
2
+
3
+ const debug = createDebug("plainblog:PostgresAdapter");
4
+ export default class PostgresAdapter {
5
+ dbtype = "postgres";
6
+ username;
7
+ password;
8
+ host;
9
+ dbname;
10
+ dbport;
11
+ pool;
12
+ ready;
13
+ constructor(options = {}) {
14
+ debug(JSON.stringify(options));
15
+ this.username = options.username;
16
+ this.password = options.password;
17
+ this.host = options.host;
18
+ this.dbname = options.dbname || "blog";
19
+ this.dbport = options.dbport || 5432;
20
+ if (!this.username || !this.password || !this.host) {
21
+ throw new Error(
22
+ "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options.",
23
+ );
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
+ } catch (error) {
34
+ if (
35
+ error.code === "ERR_MODULE_NOT_FOUND" ||
36
+ error.code === "MODULE_NOT_FOUND"
37
+ ) {
38
+ console.error(
39
+ "The 'pg' package is not installed. Please install it by running: npm install pg",
40
+ );
41
+ process.exit(1);
42
+ } else {
43
+ throw error;
44
+ }
45
+ }
46
+ const { Pool, Client } = pg.default || pg;
47
+ try {
48
+ const client = new Client({
49
+ user: this.username,
50
+ host: this.host,
51
+ database: "postgres",
52
+ password: this.password,
53
+ port: this.dbport,
54
+ });
55
+ await client.connect();
56
+ const res = await client.query(
57
+ "SELECT 1 FROM pg_database WHERE datname = $1",
58
+ [this.dbname],
59
+ );
60
+ if (res.rowCount === 0) {
61
+ await client.query(`CREATE DATABASE "${this.dbname}"`);
62
+ }
63
+ await client.end();
64
+ } catch (e) {
65
+ console.error("Failed to check/create database:", e);
66
+ }
67
+ this.pool = new Pool({
68
+ user: this.username,
69
+ host: this.host,
70
+ database: this.dbname,
71
+ password: this.password,
72
+ port: this.dbport,
73
+ });
74
+ try {
75
+ const client = await this.pool.connect();
76
+ try {
77
+ await client.query(`
78
+ CREATE TABLE IF NOT EXISTS "Articles" (
79
+ id BIGSERIAL PRIMARY KEY,
80
+ title VARCHAR(255),
81
+ content TEXT,
82
+ image TEXT,
83
+ "createdAt" TIMESTAMP,
84
+ "updatedAt" TIMESTAMP
85
+ )
86
+ `);
87
+ await client.query(`
88
+ CREATE TABLE IF NOT EXISTS "BlogInfos" (
89
+ id SERIAL PRIMARY KEY,
90
+ title VARCHAR(255)
91
+ )
92
+ `);
93
+ const res = await client.query(
94
+ 'SELECT count(*) as count FROM "BlogInfos"',
95
+ );
96
+ if (parseInt(res.rows[0].count) === 0) {
97
+ await client.query('INSERT INTO "BlogInfos" (title) VALUES ($1)', [
98
+ "My Default Blog Title",
99
+ ]);
100
+ }
101
+ this.ready = true;
102
+ } finally {
103
+ client.release();
104
+ }
105
+ } catch (err) {
106
+ console.error("Failed to initialize PostgresAdapter", err);
107
+ throw err;
108
+ }
109
+ }
110
+ async terminate() {
111
+ if (this.pool) {
112
+ await this.pool.end();
113
+ this.pool = null;
114
+ this.ready = false;
115
+ }
116
+ }
117
+ getType() {
118
+ return this.dbtype;
119
+ }
120
+ getDBName() {
121
+ return this.dbname;
122
+ }
123
+ isReady() {
124
+ return this.ready;
125
+ }
126
+ async getBlogTitle() {
127
+ if (!this.pool) await this.initialize();
128
+ const res = await this.pool.query('SELECT title FROM "BlogInfos" LIMIT 1');
129
+ return res.rows.length > 0 ? res.rows[0].title : "Blog";
130
+ }
131
+ async updateBlogTitle(newTitle) {
132
+ if (!this.pool) await this.initialize();
133
+ await this.pool.query('UPDATE "BlogInfos" SET title = $1', [newTitle]);
134
+ }
135
+ async save(newArticle) {
136
+ if (!this.pool) await this.initialize();
137
+ const createdAt = newArticle.createdAt
138
+ ? new Date(newArticle.createdAt)
139
+ : new Date();
140
+ const updatedAt = new Date();
141
+ const cols = ["title", "content", "image", '"createdAt"', '"updatedAt"'];
142
+ const vals = [
143
+ newArticle.title,
144
+ newArticle.content,
145
+ newArticle.image,
146
+ createdAt,
147
+ updatedAt,
148
+ ];
149
+ if (newArticle.id) {
150
+ cols.unshift("id");
151
+ vals.unshift(newArticle.id);
152
+ }
153
+ const finalPlaceholders = vals.map((_, i) => `$${i + 1}`);
154
+ const query = `
155
+ INSERT INTO "Articles" (${cols.join(", ")})
156
+ VALUES (${finalPlaceholders.join(", ")})
157
+ RETURNING id
158
+ `;
159
+ const res = await this.pool.query(query, vals);
160
+ const id = Number(res.rows[0].id);
161
+ return {
162
+ ...newArticle,
163
+ id,
164
+ createdAt,
165
+ updatedAt,
166
+ };
167
+ }
168
+ async update(id, data) {
169
+ if (!this.pool) await this.initialize();
170
+ const sets = [];
171
+ const values = [];
172
+ let paramIndex = 1;
173
+ if (data.title !== undefined) {
174
+ sets.push(`title = $${paramIndex++}`);
175
+ values.push(data.title);
176
+ }
177
+ if (data.content !== undefined) {
178
+ sets.push(`content = $${paramIndex++}`);
179
+ values.push(data.content);
180
+ }
181
+ if (data.image !== undefined) {
182
+ sets.push(`image = $${paramIndex++}`);
183
+ values.push(data.image);
184
+ }
185
+ sets.push(`"updatedAt" = $${paramIndex++}`);
186
+ values.push(new Date());
187
+ if (sets.length > 0) {
188
+ values.push(id);
189
+ const query = `UPDATE "Articles" SET ${sets.join(", ")} WHERE id = $${paramIndex}`;
190
+ await this.pool.query(query, values);
191
+ }
192
+ }
193
+ async remove(id) {
194
+ if (!this.pool) await this.initialize();
195
+ await this.pool.query('DELETE FROM "Articles" WHERE id = $1', [id]);
196
+ }
197
+ async findAll(
198
+ limit = 4,
199
+ offset = 0,
200
+ startId = null,
201
+ endId = null,
202
+ order = "DESC",
203
+ ) {
204
+ if (!this.pool) await this.initialize();
205
+ let query = 'SELECT * FROM "Articles"';
206
+ const conditions = [];
207
+ const values = [];
208
+ let paramIndex = 1;
209
+ const isDate = typeof startId === "string" || typeof endId === "string";
210
+ if (startId !== null && endId !== null) {
211
+ if (isDate) {
212
+ conditions.push(
213
+ `"createdAt" BETWEEN $${paramIndex++} AND $${paramIndex++}`,
214
+ );
215
+ values.push(startId, endId);
216
+ } else {
217
+ conditions.push(`id BETWEEN $${paramIndex++} AND $${paramIndex++}`);
218
+ values.push(
219
+ Math.min(Number(startId), Number(endId)),
220
+ Math.max(Number(startId), Number(endId)),
221
+ );
222
+ }
223
+ } else if (startId !== null) {
224
+ if (isDate) {
225
+ conditions.push(
226
+ order === "DESC"
227
+ ? `"createdAt" <= $${paramIndex++}`
228
+ : `"createdAt" >= $${paramIndex++}`,
229
+ );
230
+ values.push(startId);
231
+ } else {
232
+ conditions.push(
233
+ order === "DESC"
234
+ ? `id <= $${paramIndex++}`
235
+ : `id >= $${paramIndex++}`,
236
+ );
237
+ values.push(startId);
238
+ }
239
+ }
240
+ if (conditions.length > 0) {
241
+ query += " WHERE " + conditions.join(" AND ");
242
+ }
243
+ query += ` ORDER BY "createdAt" ${order}, id ${order}`;
244
+ if (limit !== -1) {
245
+ query += ` LIMIT $${paramIndex++}`;
246
+ values.push(limit);
247
+ }
248
+ if (offset > 0) {
249
+ query += ` OFFSET $${paramIndex++}`;
250
+ values.push(offset);
251
+ }
252
+ const res = await this.pool.query(query, values);
253
+ return res.rows.map((row) => ({
254
+ id: Number(row.id),
255
+ title: row.title,
256
+ content: row.content,
257
+ image: row.image,
258
+ createdAt: new Date(row.createdAt),
259
+ updatedAt: new Date(row.updatedAt),
260
+ }));
261
+ }
262
+ }
@@ -1,97 +1,262 @@
1
- import SequelizeAdapter from "./SequelizeAdapter.js";
2
- import { createDebug } from "../debug-loader.js";
3
-
4
- const debug = createDebug("plainblog:SqliteAdapter");
5
-
6
- export default class PostgresAdapter extends SequelizeAdapter {
7
- dbtype = "postgres";
8
-
9
- constructor(options = {}) {
10
- super();
11
- debug(JSON.stringify(options));
12
- this.username = options.username;
13
- this.password = options.password;
14
- this.host = options.host;
15
- if (options.dbname) this.dbname = options.dbname;
16
- if (options.dbport) this.dbport = options.dbport;
17
- else this.dbport = 5432;
18
- if (!this.username || !this.password || !this.host) {
19
- throw new Error(
20
- "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options.",
21
- );
22
- }
23
- this.ready = false;
24
- }
25
-
26
- async initialize() {
27
- console.log("initialize database");
28
- let pgPkg;
29
- try {
30
- await import("sequelize");
31
- pgPkg = await import("pg");
32
- await import("pg-hstore");
33
- } catch (error) {
34
- if (
35
- error.code === "ERR_MODULE_NOT_FOUND" ||
36
- error.code === "MODULE_NOT_FOUND"
37
- ) {
38
- console.error(
39
- "The 'sequelize', 'pg', and 'pg-hstore' packages are not installed. Please install them by running: npm install sequelize pg pg-hstore",
40
- );
41
- throw new Error(
42
- "Missing optional dependencies: 'sequelize', 'pg', 'pg-hstore'",
43
- );
44
- }
45
- }
46
- await this.loadSequelize();
47
- const maxRetries = 10;
48
- const retryDelay = 3000;
49
-
50
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
51
- try {
52
- console.log(
53
- `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
54
- );
55
-
56
- this.sequelize = new this.Sequelize(
57
- `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
58
- {
59
- logging: false,
60
- dialectModule: pgPkg.default || pgPkg,
61
- },
62
- );
63
- await this.sequelize.authenticate();
64
- await this.initializeModels();
65
- this.ready = true;
66
- return;
67
- } catch (err) {
68
- if (err.message.includes("Please install")) {
69
- throw new Error(
70
- "PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore",
71
- );
72
- }
73
- console.error(
74
- `Database connection attempt ${attempt} failed: ${err.message}`,
75
- );
76
- if (attempt === maxRetries) {
77
- console.error("Max retries reached. Exiting.");
78
- throw err;
79
- }
80
- console.log(`retrying in ${retryDelay / 1000} seconds...`);
81
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
82
- }
83
- }
84
- }
85
-
86
- getType() {
87
- return this.dbtype;
88
- }
89
-
90
- getDBName() {
91
- return this.dbname;
92
- }
93
-
94
- isReady() {
95
- return this.ready;
96
- }
97
- }
1
+ import { createDebug } from "../debug-loader.js";
2
+
3
+ const debug = createDebug("plainblog:PostgresAdapter");
4
+ export default class PostgresAdapter {
5
+ dbtype = "postgres";
6
+ username;
7
+ password;
8
+ host;
9
+ dbname;
10
+ dbport;
11
+ pool;
12
+ ready;
13
+ constructor(options = {}) {
14
+ debug(JSON.stringify(options));
15
+ this.username = options.username;
16
+ this.password = options.password;
17
+ this.host = options.host;
18
+ this.dbname = options.dbname || "blog";
19
+ this.dbport = options.dbport || 5432;
20
+ if (!this.username || !this.password || !this.host) {
21
+ throw new Error(
22
+ "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options.",
23
+ );
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
+ } catch (error) {
34
+ if (
35
+ error.code === "ERR_MODULE_NOT_FOUND" ||
36
+ error.code === "MODULE_NOT_FOUND"
37
+ ) {
38
+ console.error(
39
+ "The 'pg' package is not installed. Please install it by running: npm install pg",
40
+ );
41
+ process.exit(1);
42
+ } else {
43
+ throw error;
44
+ }
45
+ }
46
+ const { Pool, Client } = pg.default || pg;
47
+ try {
48
+ const client = new Client({
49
+ user: this.username,
50
+ host: this.host,
51
+ database: "postgres",
52
+ password: this.password,
53
+ port: this.dbport,
54
+ });
55
+ await client.connect();
56
+ const res = await client.query(
57
+ "SELECT 1 FROM pg_database WHERE datname = $1",
58
+ [this.dbname],
59
+ );
60
+ if (res.rowCount === 0) {
61
+ await client.query(`CREATE DATABASE "${this.dbname}"`);
62
+ }
63
+ await client.end();
64
+ } catch (e) {
65
+ console.error("Failed to check/create database:", e);
66
+ }
67
+ this.pool = new Pool({
68
+ user: this.username,
69
+ host: this.host,
70
+ database: this.dbname,
71
+ password: this.password,
72
+ port: this.dbport,
73
+ });
74
+ try {
75
+ const client = await this.pool.connect();
76
+ try {
77
+ await client.query(`
78
+ CREATE TABLE IF NOT EXISTS "Articles" (
79
+ id BIGSERIAL PRIMARY KEY,
80
+ title VARCHAR(255),
81
+ content TEXT,
82
+ image TEXT,
83
+ "createdAt" TIMESTAMP,
84
+ "updatedAt" TIMESTAMP
85
+ )
86
+ `);
87
+ await client.query(`
88
+ CREATE TABLE IF NOT EXISTS "BlogInfos" (
89
+ id SERIAL PRIMARY KEY,
90
+ title VARCHAR(255)
91
+ )
92
+ `);
93
+ const res = await client.query(
94
+ 'SELECT count(*) as count FROM "BlogInfos"',
95
+ );
96
+ if (parseInt(res.rows[0].count) === 0) {
97
+ await client.query('INSERT INTO "BlogInfos" (title) VALUES ($1)', [
98
+ "My Default Blog Title",
99
+ ]);
100
+ }
101
+ this.ready = true;
102
+ } finally {
103
+ client.release();
104
+ }
105
+ } catch (err) {
106
+ console.error("Failed to initialize PostgresAdapter", err);
107
+ throw err;
108
+ }
109
+ }
110
+ async terminate() {
111
+ if (this.pool) {
112
+ await this.pool.end();
113
+ this.pool = null;
114
+ this.ready = false;
115
+ }
116
+ }
117
+ getType() {
118
+ return this.dbtype;
119
+ }
120
+ getDBName() {
121
+ return this.dbname;
122
+ }
123
+ isReady() {
124
+ return this.ready;
125
+ }
126
+ async getBlogTitle() {
127
+ if (!this.pool) await this.initialize();
128
+ const res = await this.pool.query('SELECT title FROM "BlogInfos" LIMIT 1');
129
+ return res.rows.length > 0 ? res.rows[0].title : "Blog";
130
+ }
131
+ async updateBlogTitle(newTitle) {
132
+ if (!this.pool) await this.initialize();
133
+ await this.pool.query('UPDATE "BlogInfos" SET title = $1', [newTitle]);
134
+ }
135
+ async save(newArticle) {
136
+ if (!this.pool) await this.initialize();
137
+ const createdAt = newArticle.createdAt
138
+ ? new Date(newArticle.createdAt)
139
+ : new Date();
140
+ const updatedAt = new Date();
141
+ const cols = ["title", "content", "image", '"createdAt"', '"updatedAt"'];
142
+ const vals = [
143
+ newArticle.title,
144
+ newArticle.content,
145
+ newArticle.image,
146
+ createdAt,
147
+ updatedAt,
148
+ ];
149
+ if (newArticle.id) {
150
+ cols.unshift("id");
151
+ vals.unshift(newArticle.id);
152
+ }
153
+ const finalPlaceholders = vals.map((_, i) => `$${i + 1}`);
154
+ const query = `
155
+ INSERT INTO "Articles" (${cols.join(", ")})
156
+ VALUES (${finalPlaceholders.join(", ")})
157
+ RETURNING id
158
+ `;
159
+ const res = await this.pool.query(query, vals);
160
+ const id = Number(res.rows[0].id);
161
+ return {
162
+ ...newArticle,
163
+ id,
164
+ createdAt,
165
+ updatedAt,
166
+ };
167
+ }
168
+ async update(id, data) {
169
+ if (!this.pool) await this.initialize();
170
+ const sets = [];
171
+ const values = [];
172
+ let paramIndex = 1;
173
+ if (data.title !== undefined) {
174
+ sets.push(`title = $${paramIndex++}`);
175
+ values.push(data.title);
176
+ }
177
+ if (data.content !== undefined) {
178
+ sets.push(`content = $${paramIndex++}`);
179
+ values.push(data.content);
180
+ }
181
+ if (data.image !== undefined) {
182
+ sets.push(`image = $${paramIndex++}`);
183
+ values.push(data.image);
184
+ }
185
+ sets.push(`"updatedAt" = $${paramIndex++}`);
186
+ values.push(new Date());
187
+ if (sets.length > 0) {
188
+ values.push(id);
189
+ const query = `UPDATE "Articles" SET ${sets.join(", ")} WHERE id = $${paramIndex}`;
190
+ await this.pool.query(query, values);
191
+ }
192
+ }
193
+ async remove(id) {
194
+ if (!this.pool) await this.initialize();
195
+ await this.pool.query('DELETE FROM "Articles" WHERE id = $1', [id]);
196
+ }
197
+ async findAll(
198
+ limit = 4,
199
+ offset = 0,
200
+ startId = null,
201
+ endId = null,
202
+ order = "DESC",
203
+ ) {
204
+ if (!this.pool) await this.initialize();
205
+ let query = 'SELECT * FROM "Articles"';
206
+ const conditions = [];
207
+ const values = [];
208
+ let paramIndex = 1;
209
+ const isDate = typeof startId === "string" || typeof endId === "string";
210
+ if (startId !== null && endId !== null) {
211
+ if (isDate) {
212
+ conditions.push(
213
+ `"createdAt" BETWEEN $${paramIndex++} AND $${paramIndex++}`,
214
+ );
215
+ values.push(startId, endId);
216
+ } else {
217
+ conditions.push(`id BETWEEN $${paramIndex++} AND $${paramIndex++}`);
218
+ values.push(
219
+ Math.min(Number(startId), Number(endId)),
220
+ Math.max(Number(startId), Number(endId)),
221
+ );
222
+ }
223
+ } else if (startId !== null) {
224
+ if (isDate) {
225
+ conditions.push(
226
+ order === "DESC"
227
+ ? `"createdAt" <= $${paramIndex++}`
228
+ : `"createdAt" >= $${paramIndex++}`,
229
+ );
230
+ values.push(startId);
231
+ } else {
232
+ conditions.push(
233
+ order === "DESC"
234
+ ? `id <= $${paramIndex++}`
235
+ : `id >= $${paramIndex++}`,
236
+ );
237
+ values.push(startId);
238
+ }
239
+ }
240
+ if (conditions.length > 0) {
241
+ query += " WHERE " + conditions.join(" AND ");
242
+ }
243
+ query += ` ORDER BY "createdAt" ${order}, id ${order}`;
244
+ if (limit !== -1) {
245
+ query += ` LIMIT $${paramIndex++}`;
246
+ values.push(limit);
247
+ }
248
+ if (offset > 0) {
249
+ query += ` OFFSET $${paramIndex++}`;
250
+ values.push(offset);
251
+ }
252
+ const res = await this.pool.query(query, values);
253
+ return res.rows.map((row) => ({
254
+ id: Number(row.id),
255
+ title: row.title,
256
+ content: row.content,
257
+ image: row.image,
258
+ createdAt: new Date(row.createdAt),
259
+ updatedAt: new Date(row.updatedAt),
260
+ }));
261
+ }
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -8,8 +8,8 @@
8
8
  "dev": "node index.js",
9
9
  "cluster": "node cluster-server.js",
10
10
  "postinstall": "node postinstall.js",
11
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
12
- "test2": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
11
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
12
+ "test2": "node --experimental-vm-modules node_modules/jest/bin/jest.js --silent --runInBand",
13
13
  "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage & start coverage/lcov-report/index.html",
14
14
  "view-coverage": "start coverage/lcov-report/index.html",
15
15
  "lint": "eslint .",
@@ -28,12 +28,12 @@
28
28
  "@types/node": "^25.0.3",
29
29
  "debug": "^4.4.3",
30
30
  "eslint": "^9.39.2",
31
- "eslint-plugin-jest": "^28.6.0",
31
+ "eslint-plugin-jest": "^28.14.0",
32
32
  "eslint-plugin-promise": "^7.2.1",
33
33
  "globals": "^17.0.0",
34
34
  "jest": "^29.7.0",
35
35
  "multer": "^2.0.2",
36
- "pg": "^8.16.3",
36
+ "pg": "^8.18.0",
37
37
  "pg-hstore": "^2.3.4",
38
38
  "sequelize": "^6.37.7",
39
39
  "typescript": "^5.9.3",
package/profile-bst.js CHANGED
@@ -11,7 +11,7 @@ import fs from "node:fs";
11
11
 
12
12
  async function runSearch(name, BlogClass, cb = () => {}) {
13
13
  const storage = new BlogClass();
14
- let allDates = generateDateList("2000-01-01T00:00", "2025-12-25T00:00");
14
+ let allDates = [...generateDateList("2000-01-01T00:00", "2025-12-25T00:00")];
15
15
 
16
16
  // The callback (which shuffles for BinaryTreeSearch) must be called BEFORE insertion
17
17
  cb(allDates);
@@ -0,0 +1,311 @@
1
+ import { DatabaseAdapter } from "./DatabaseAdapter.js";
2
+ import { Article } from "./Article.interface.js";
3
+ //import { createDebug } from "../../debug-loader.js";
4
+ const createDebug = (namespace: any) => {
5
+ return (...args: any) => {};
6
+ };
7
+
8
+ const debug = createDebug("plainblog:PostgresAdapter");
9
+
10
+ interface PostgresAdapterOptions {
11
+ username?: string;
12
+ password?: string;
13
+ host?: string;
14
+ dbname?: string;
15
+ dbport?: number;
16
+ }
17
+
18
+ export default class PostgresAdapter implements DatabaseAdapter {
19
+ dbtype = "postgres";
20
+ username?: string;
21
+ password?: string;
22
+ host?: string;
23
+ dbname: string;
24
+ dbport: number;
25
+ pool: any;
26
+ ready: boolean;
27
+
28
+ constructor(options: PostgresAdapterOptions = {}) {
29
+ debug(JSON.stringify(options));
30
+ this.username = options.username;
31
+ this.password = options.password;
32
+ this.host = options.host;
33
+ this.dbname = options.dbname || "blog";
34
+ this.dbport = options.dbport || 5432;
35
+
36
+ if (!this.username || !this.password || !this.host) {
37
+ throw new Error(
38
+ "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options.",
39
+ );
40
+ }
41
+ this.ready = false;
42
+ this.pool = null;
43
+ }
44
+
45
+ async initialize(): Promise<void> {
46
+ let pg: any;
47
+ try {
48
+ // @ts-ignore
49
+ pg = await import("pg");
50
+ } catch (error: any) {
51
+ if (
52
+ error.code === "ERR_MODULE_NOT_FOUND" ||
53
+ error.code === "MODULE_NOT_FOUND"
54
+ ) {
55
+ console.error(
56
+ "The 'pg' package is not installed. Please install it by running: npm install pg",
57
+ );
58
+ process.exit(1);
59
+ } else {
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ const { Pool, Client } = pg.default || pg;
65
+
66
+ try {
67
+ const client = new Client({
68
+ user: this.username,
69
+ host: this.host,
70
+ database: "postgres",
71
+ password: this.password,
72
+ port: this.dbport,
73
+ });
74
+ await client.connect();
75
+ const res = await client.query(
76
+ "SELECT 1 FROM pg_database WHERE datname = $1",
77
+ [this.dbname],
78
+ );
79
+ if (res.rowCount === 0) {
80
+ await client.query(`CREATE DATABASE "${this.dbname}"`);
81
+ }
82
+ await client.end();
83
+ } catch (e) {
84
+ console.error("Failed to check/create database:", e);
85
+ }
86
+
87
+ this.pool = new Pool({
88
+ user: this.username,
89
+ host: this.host,
90
+ database: this.dbname,
91
+ password: this.password,
92
+ port: this.dbport,
93
+ });
94
+
95
+ try {
96
+ const client = await this.pool.connect();
97
+ try {
98
+ await client.query(`
99
+ CREATE TABLE IF NOT EXISTS "Articles" (
100
+ id BIGSERIAL PRIMARY KEY,
101
+ title VARCHAR(255),
102
+ content TEXT,
103
+ image TEXT,
104
+ "createdAt" TIMESTAMP,
105
+ "updatedAt" TIMESTAMP
106
+ )
107
+ `);
108
+ await client.query(`
109
+ CREATE TABLE IF NOT EXISTS "BlogInfos" (
110
+ id SERIAL PRIMARY KEY,
111
+ title VARCHAR(255)
112
+ )
113
+ `);
114
+
115
+ const res = await client.query(
116
+ 'SELECT count(*) as count FROM "BlogInfos"',
117
+ );
118
+ if (parseInt(res.rows[0].count) === 0) {
119
+ await client.query(
120
+ 'INSERT INTO "BlogInfos" (title) VALUES ($1)',
121
+ ["My Default Blog Title"],
122
+ );
123
+ }
124
+ this.ready = true;
125
+ } finally {
126
+ client.release();
127
+ }
128
+ } catch (err: any) {
129
+ console.error("Failed to initialize PostgresAdapter", err);
130
+ throw err;
131
+ }
132
+ }
133
+
134
+ async terminate(): Promise<void> {
135
+ if (this.pool) {
136
+ await this.pool.end();
137
+ this.pool = null;
138
+ this.ready = false;
139
+ }
140
+ }
141
+
142
+ getType(): string {
143
+ return this.dbtype;
144
+ }
145
+
146
+ getDBName(): string {
147
+ return this.dbname;
148
+ }
149
+
150
+ isReady(): boolean {
151
+ return this.ready;
152
+ }
153
+
154
+ async getBlogTitle(): Promise<string> {
155
+ if (!this.pool) await this.initialize();
156
+ const res = await this.pool.query(
157
+ 'SELECT title FROM "BlogInfos" LIMIT 1',
158
+ );
159
+ return res.rows.length > 0 ? res.rows[0].title : "Blog";
160
+ }
161
+
162
+ async updateBlogTitle(newTitle: string): Promise<void> {
163
+ if (!this.pool) await this.initialize();
164
+ await this.pool.query('UPDATE "BlogInfos" SET title = $1', [newTitle]);
165
+ }
166
+
167
+ async save(newArticle: Article): Promise<Article> {
168
+ if (!this.pool) await this.initialize();
169
+ const createdAt = newArticle.createdAt
170
+ ? new Date(newArticle.createdAt)
171
+ : new Date();
172
+ const updatedAt = new Date();
173
+
174
+ const cols = ['title', 'content', 'image', '"createdAt"', '"updatedAt"'];
175
+ const vals: any[] = [
176
+ newArticle.title,
177
+ newArticle.content,
178
+ (newArticle as any).image,
179
+ createdAt,
180
+ updatedAt,
181
+ ];
182
+
183
+ if (newArticle.id) {
184
+ cols.unshift("id");
185
+ vals.unshift(newArticle.id);
186
+ }
187
+
188
+ const finalPlaceholders = vals.map((_, i) => `$${i + 1}`);
189
+
190
+ const query = `
191
+ INSERT INTO "Articles" (${cols.join(", ")})
192
+ VALUES (${finalPlaceholders.join(", ")})
193
+ RETURNING id
194
+ `;
195
+
196
+ const res = await this.pool.query(query, vals);
197
+ const id = Number(res.rows[0].id);
198
+
199
+ return {
200
+ ...newArticle,
201
+ id,
202
+ createdAt,
203
+ updatedAt,
204
+ };
205
+ }
206
+
207
+ async update(id: number, data: Partial<Article>): Promise<void> {
208
+ if (!this.pool) await this.initialize();
209
+ const sets: string[] = [];
210
+ const values: any[] = [];
211
+ let paramIndex = 1;
212
+
213
+ if (data.title !== undefined) {
214
+ sets.push(`title = $${paramIndex++}`);
215
+ values.push(data.title);
216
+ }
217
+ if (data.content !== undefined) {
218
+ sets.push(`content = $${paramIndex++}`);
219
+ values.push(data.content);
220
+ }
221
+ if ((data as any).image !== undefined) {
222
+ sets.push(`image = $${paramIndex++}`);
223
+ values.push((data as any).image);
224
+ }
225
+
226
+ sets.push(`"updatedAt" = $${paramIndex++}`);
227
+ values.push(new Date());
228
+
229
+ if (sets.length > 0) {
230
+ values.push(id);
231
+ const query = `UPDATE "Articles" SET ${sets.join(", ")} WHERE id = $${paramIndex}`;
232
+ await this.pool.query(query, values);
233
+ }
234
+ }
235
+
236
+ async remove(id: number): Promise<void> {
237
+ if (!this.pool) await this.initialize();
238
+ await this.pool.query('DELETE FROM "Articles" WHERE id = $1', [id]);
239
+ }
240
+
241
+ async findAll(
242
+ limit: number = 4,
243
+ offset: number = 0,
244
+ startId: number | string | null = null,
245
+ endId: number | string | null = null,
246
+ order: "DESC" | "ASC" = "DESC",
247
+ ): Promise<Article[]> {
248
+ if (!this.pool) await this.initialize();
249
+
250
+ let query = 'SELECT * FROM "Articles"';
251
+ const conditions: string[] = [];
252
+ const values: any[] = [];
253
+ let paramIndex = 1;
254
+
255
+ const isDate = typeof startId === "string" || typeof endId === "string";
256
+
257
+ if (startId !== null && endId !== null) {
258
+ if (isDate) {
259
+ conditions.push(
260
+ `"createdAt" BETWEEN $${paramIndex++} AND $${paramIndex++}`,
261
+ );
262
+ values.push(startId, endId);
263
+ } else {
264
+ conditions.push(`id BETWEEN $${paramIndex++} AND $${paramIndex++}`);
265
+ values.push(
266
+ Math.min(Number(startId), Number(endId)),
267
+ Math.max(Number(startId), Number(endId)),
268
+ );
269
+ }
270
+ } else if (startId !== null) {
271
+ if (isDate) {
272
+ conditions.push(
273
+ order === "DESC"
274
+ ? `"createdAt" <= $${paramIndex++}`
275
+ : `"createdAt" >= $${paramIndex++}`,
276
+ );
277
+ values.push(startId);
278
+ } else {
279
+ conditions.push(
280
+ order === "DESC" ? `id <= $${paramIndex++}` : `id >= $${paramIndex++}`,
281
+ );
282
+ values.push(startId);
283
+ }
284
+ }
285
+
286
+ if (conditions.length > 0) {
287
+ query += " WHERE " + conditions.join(" AND ");
288
+ }
289
+
290
+ query += ` ORDER BY "createdAt" ${order}, id ${order}`;
291
+
292
+ if (limit !== -1) {
293
+ query += ` LIMIT $${paramIndex++}`;
294
+ values.push(limit);
295
+ }
296
+ if (offset > 0) {
297
+ query += ` OFFSET $${paramIndex++}`;
298
+ values.push(offset);
299
+ }
300
+
301
+ const res = await this.pool.query(query, values);
302
+ return res.rows.map((row: any) => ({
303
+ id: Number(row.id),
304
+ title: row.title,
305
+ content: row.content,
306
+ image: row.image,
307
+ createdAt: new Date(row.createdAt),
308
+ updatedAt: new Date(row.updatedAt),
309
+ }));
310
+ }
311
+ }