@lexho111/plainblog 0.8.5 → 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/dist/model/PostgresAdapter.js +262 -0
- package/model/PostgresAdapter.js +262 -97
- package/package.json +5 -5
- package/profile-bst.js +1 -1
- package/src1/model/PostgresAdapter.ts +311 -0
|
@@ -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
|
+
}
|
package/model/PostgresAdapter.js
CHANGED
|
@@ -1,97 +1,262 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
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 '
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|