• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CMU-17313Q / fall23-nodebb-generation-z / 6681375918

29 Oct 2023 05:07AM UTC coverage: 84.977% (+0.02%) from 84.959%
6681375918

Pull #42

github

web-flow
Merge ef6a9c667 into 68652f413
Pull Request #42: Integration-Prettier-Into-Github-Action-Flows

10228 of 13274 branches covered (0.0%)

Branch coverage included in aggregate %.

2332 of 2504 new or added lines in 42 files covered. (93.13%)

22 existing lines in 7 files now uncovered.

22613 of 25373 relevant lines covered (89.12%)

3125.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.73
/src/database/postgres.js
1
'use strict';
2

3
const winston = require('winston');
5✔
4
const async = require('async');
5✔
5
const nconf = require('nconf');
5✔
6
const session = require('express-session');
5✔
7
const semver = require('semver');
5✔
8

9
const connection = require('./postgres/connection');
5✔
10

11
const postgresModule = module.exports;
5✔
12

13
postgresModule.questions = [
5✔
14
    {
15
        name: 'postgres:host',
16
        description: 'Host IP or address of your PostgreSQL instance',
17
        default: nconf.get('postgres:host') || '127.0.0.1',
5!
18
    },
19
    {
20
        name: 'postgres:port',
21
        description: 'Host port of your PostgreSQL instance',
22
        default: nconf.get('postgres:port') || 5432,
5!
23
    },
24
    {
25
        name: 'postgres:username',
26
        description: 'PostgreSQL username',
27
        default: nconf.get('postgres:username') || '',
5!
28
    },
29
    {
30
        name: 'postgres:password',
31
        description: 'Password of your PostgreSQL database',
32
        hidden: true,
33
        default: nconf.get('postgres:password') || '',
5!
NEW
34
        before: function (value) { value = value || nconf.get('postgres:password') || ''; return value; },
×
35
    },
36
    {
37
        name: 'postgres:database',
38
        description: 'PostgreSQL database name',
39
        default: nconf.get('postgres:database') || 'nodebb',
5!
40
    },
41
    {
42
        name: 'postgres:ssl',
43
        description: 'Enable SSL for PostgreSQL database access',
44
        default: nconf.get('postgres:ssl') || false,
10✔
45
    },
46
];
47

48
postgresModule.init = async function () {
5✔
49
    const { Pool } = require('pg');
13✔
50
    const connOptions = connection.getConnectionOptions();
13✔
51
    const pool = new Pool(connOptions);
13✔
52
    postgresModule.pool = pool;
13✔
53
    postgresModule.client = pool;
13✔
54
    const client = await pool.connect();
13✔
55
    try {
13✔
56
        await checkUpgrade(client);
13✔
57
    } catch (err) {
NEW
58
        winston.error(`NodeBB could not connect to your PostgreSQL database. PostgreSQL returned the following error: ${err.message}`);
×
NEW
59
        throw err;
×
60
    } finally {
61
        client.release();
13✔
62
    }
63
};
64

65

66
async function checkUpgrade(client) {
67
    const res = await client.query(`
13✔
68
SELECT EXISTS(SELECT *
69
                FROM "information_schema"."columns"
70
               WHERE "table_schema" = 'public'
71
                 AND "table_name" = 'objects'
72
                 AND "column_name" = 'data') a,
73
       EXISTS(SELECT *
74
                FROM "information_schema"."columns"
75
               WHERE "table_schema" = 'public'
76
                 AND "table_name" = 'legacy_hash'
77
                 AND "column_name" = '_key') b,
78
       EXISTS(SELECT *
79
                FROM "information_schema"."routines"
80
               WHERE "routine_schema" = 'public'
81
                 AND "routine_name" = 'nodebb_get_sorted_set_members') c`);
82

83
    if (res.rows[0].a && res.rows[0].b && res.rows[0].c) {
13!
UNCOV
84
        return;
×
85
    }
86

87
    await client.query(`BEGIN`);
13✔
88
    try {
13✔
89
        if (!res.rows[0].b) {
13✔
90
            await client.query(`
1✔
91
CREATE TYPE LEGACY_OBJECT_TYPE AS ENUM (
92
    'hash', 'zset', 'set', 'list', 'string'
93
)`);
94
            await client.query(`
1✔
95
CREATE TABLE "legacy_object" (
96
    "_key" TEXT NOT NULL
97
        PRIMARY KEY,
98
    "type" LEGACY_OBJECT_TYPE NOT NULL,
99
    "expireAt" TIMESTAMPTZ DEFAULT NULL,
100
    UNIQUE ( "_key", "type" )
101
)`);
102
            await client.query(`
1✔
103
CREATE TABLE "legacy_hash" (
104
    "_key" TEXT NOT NULL
105
        PRIMARY KEY,
106
    "data" JSONB NOT NULL,
107
    "type" LEGACY_OBJECT_TYPE NOT NULL
108
        DEFAULT 'hash'::LEGACY_OBJECT_TYPE
109
        CHECK ( "type" = 'hash' ),
110
    CONSTRAINT "fk__legacy_hash__key"
111
        FOREIGN KEY ("_key", "type")
112
        REFERENCES "legacy_object"("_key", "type")
113
        ON UPDATE CASCADE
114
        ON DELETE CASCADE
115
)`);
116
            await client.query(`
1✔
117
CREATE TABLE "legacy_zset" (
118
    "_key" TEXT NOT NULL,
119
    "value" TEXT NOT NULL,
120
    "score" NUMERIC NOT NULL,
121
    "type" LEGACY_OBJECT_TYPE NOT NULL
122
        DEFAULT 'zset'::LEGACY_OBJECT_TYPE
123
        CHECK ( "type" = 'zset' ),
124
    PRIMARY KEY ("_key", "value"),
125
    CONSTRAINT "fk__legacy_zset__key"
126
        FOREIGN KEY ("_key", "type")
127
        REFERENCES "legacy_object"("_key", "type")
128
        ON UPDATE CASCADE
129
        ON DELETE CASCADE
130
)`);
131
            await client.query(`
1✔
132
CREATE TABLE "legacy_set" (
133
    "_key" TEXT NOT NULL,
134
    "member" TEXT NOT NULL,
135
    "type" LEGACY_OBJECT_TYPE NOT NULL
136
        DEFAULT 'set'::LEGACY_OBJECT_TYPE
137
        CHECK ( "type" = 'set' ),
138
    PRIMARY KEY ("_key", "member"),
139
    CONSTRAINT "fk__legacy_set__key"
140
        FOREIGN KEY ("_key", "type")
141
        REFERENCES "legacy_object"("_key", "type")
142
        ON UPDATE CASCADE
143
        ON DELETE CASCADE
144
)`);
145
            await client.query(`
1✔
146
CREATE TABLE "legacy_list" (
147
    "_key" TEXT NOT NULL
148
        PRIMARY KEY,
149
    "array" TEXT[] NOT NULL,
150
    "type" LEGACY_OBJECT_TYPE NOT NULL
151
        DEFAULT 'list'::LEGACY_OBJECT_TYPE
152
        CHECK ( "type" = 'list' ),
153
    CONSTRAINT "fk__legacy_list__key"
154
        FOREIGN KEY ("_key", "type")
155
        REFERENCES "legacy_object"("_key", "type")
156
        ON UPDATE CASCADE
157
        ON DELETE CASCADE
158
)`);
159
            await client.query(`
1✔
160
CREATE TABLE "legacy_string" (
161
    "_key" TEXT NOT NULL
162
        PRIMARY KEY,
163
    "data" TEXT NOT NULL,
164
    "type" LEGACY_OBJECT_TYPE NOT NULL
165
        DEFAULT 'string'::LEGACY_OBJECT_TYPE
166
        CHECK ( "type" = 'string' ),
167
    CONSTRAINT "fk__legacy_string__key"
168
        FOREIGN KEY ("_key", "type")
169
        REFERENCES "legacy_object"("_key", "type")
170
        ON UPDATE CASCADE
171
        ON DELETE CASCADE
172
)`);
173

174
            if (res.rows[0].a) {
1!
UNCOV
175
                await client.query(`
×
176
INSERT INTO "legacy_object" ("_key", "type", "expireAt")
177
SELECT DISTINCT "data"->>'_key',
178
                CASE WHEN (SELECT COUNT(*)
179
                             FROM jsonb_object_keys("data" - 'expireAt')) = 2
180
                     THEN CASE WHEN ("data" ? 'value')
181
                                 OR ("data" ? 'data')
182
                               THEN 'string'
183
                               WHEN "data" ? 'array'
184
                               THEN 'list'
185
                               WHEN "data" ? 'members'
186
                               THEN 'set'
187
                               ELSE 'hash'
188
                          END
189
                     WHEN (SELECT COUNT(*)
190
                             FROM jsonb_object_keys("data" - 'expireAt')) = 3
191
                     THEN CASE WHEN ("data" ? 'value')
192
                                AND ("data" ? 'score')
193
                               THEN 'zset'
194
                               ELSE 'hash'
195
                          END
196
                     ELSE 'hash'
197
                END::LEGACY_OBJECT_TYPE,
198
                CASE WHEN ("data" ? 'expireAt')
199
                     THEN to_timestamp(("data"->>'expireAt')::double precision / 1000)
200
                     ELSE NULL
201
                END
202
  FROM "objects"`);
UNCOV
203
                await client.query(`
×
204
INSERT INTO "legacy_hash" ("_key", "data")
205
SELECT "data"->>'_key',
206
       "data" - '_key' - 'expireAt'
207
  FROM "objects"
208
 WHERE CASE WHEN (SELECT COUNT(*)
209
                    FROM jsonb_object_keys("data" - 'expireAt')) = 2
210
            THEN NOT (("data" ? 'value')
211
                   OR ("data" ? 'data')
212
                   OR ("data" ? 'members')
213
                   OR ("data" ? 'array'))
214
            WHEN (SELECT COUNT(*)
215
                    FROM jsonb_object_keys("data" - 'expireAt')) = 3
216
            THEN NOT (("data" ? 'value')
217
                  AND ("data" ? 'score'))
218
            ELSE TRUE
219
       END`);
UNCOV
220
                await client.query(`
×
221
INSERT INTO "legacy_zset" ("_key", "value", "score")
222
SELECT "data"->>'_key',
223
       "data"->>'value',
224
       ("data"->>'score')::NUMERIC
225
  FROM "objects"
226
 WHERE (SELECT COUNT(*)
227
          FROM jsonb_object_keys("data" - 'expireAt')) = 3
228
   AND ("data" ? 'value')
229
   AND ("data" ? 'score')`);
UNCOV
230
                await client.query(`
×
231
INSERT INTO "legacy_set" ("_key", "member")
232
SELECT "data"->>'_key',
233
       jsonb_array_elements_text("data"->'members')
234
  FROM "objects"
235
 WHERE (SELECT COUNT(*)
236
          FROM jsonb_object_keys("data" - 'expireAt')) = 2
237
   AND ("data" ? 'members')`);
UNCOV
238
                await client.query(`
×
239
INSERT INTO "legacy_list" ("_key", "array")
240
SELECT "data"->>'_key',
241
       ARRAY(SELECT t
242
               FROM jsonb_array_elements_text("data"->'list') WITH ORDINALITY l(t, i)
243
              ORDER BY i ASC)
244
  FROM "objects"
245
 WHERE (SELECT COUNT(*)
246
          FROM jsonb_object_keys("data" - 'expireAt')) = 2
247
   AND ("data" ? 'array')`);
UNCOV
248
                await client.query(`
×
249
INSERT INTO "legacy_string" ("_key", "data")
250
SELECT "data"->>'_key',
251
       CASE WHEN "data" ? 'value'
252
            THEN "data"->>'value'
253
            ELSE "data"->>'data'
254
       END
255
  FROM "objects"
256
 WHERE (SELECT COUNT(*)
257
          FROM jsonb_object_keys("data" - 'expireAt')) = 2
258
   AND (("data" ? 'value')
259
     OR ("data" ? 'data'))`);
UNCOV
260
                await client.query(`DROP TABLE "objects" CASCADE`);
×
UNCOV
261
                await client.query(`DROP FUNCTION "fun__objects__expireAt"() CASCADE`);
×
262
            }
263
            await client.query(`
1✔
264
CREATE VIEW "legacy_object_live" AS
265
SELECT "_key", "type"
266
  FROM "legacy_object"
267
 WHERE "expireAt" IS NULL
268
    OR "expireAt" > CURRENT_TIMESTAMP`);
269
        }
270

271
        if (!res.rows[0].c) {
13✔
272
            await client.query(`
1✔
273
CREATE FUNCTION "nodebb_get_sorted_set_members"(TEXT) RETURNS TEXT[] AS $$
274
    SELECT array_agg(z."value" ORDER BY z."score" ASC)
275
      FROM "legacy_object_live" o
276
     INNER JOIN "legacy_zset" z
277
             ON o."_key" = z."_key"
278
            AND o."type" = z."type"
279
          WHERE o."_key" = $1
280
$$ LANGUAGE sql
281
STABLE
282
STRICT
283
PARALLEL SAFE`);
284
        }
285
    } catch (ex) {
UNCOV
286
        await client.query(`ROLLBACK`);
×
UNCOV
287
        throw ex;
×
288
    }
289
    await client.query(`COMMIT`);
13✔
290
}
291

292
postgresModule.createSessionStore = async function (options) {
5✔
293
    const meta = require('../meta');
2✔
294

295
    function done(db) {
296
        const sessionStore = require('connect-pg-simple')(session);
2✔
297
        return new sessionStore({
2✔
298
            pool: db,
299
            ttl: meta.getSessionTTLSeconds(),
300
            pruneSessionInterval: nconf.get('isPrimary') ? 60 : false,
2!
301
        });
302
    }
303

304
    const db = await connection.connect(options);
2✔
305

306
    if (!nconf.get('isPrimary')) {
2!
UNCOV
307
        return done(db);
×
308
    }
309

310
    await db.query(`
2✔
311
CREATE TABLE IF NOT EXISTS "session" (
312
    "sid" CHAR(32) NOT NULL
313
        COLLATE "C"
314
        PRIMARY KEY,
315
    "sess" JSONB NOT NULL,
316
    "expire" TIMESTAMPTZ NOT NULL
317
) WITHOUT OIDS;
318

319
CREATE INDEX IF NOT EXISTS "session_expire_idx" ON "session"("expire");
320

321
ALTER TABLE "session"
322
    ALTER "sid" SET STORAGE MAIN,
323
    CLUSTER ON "session_expire_idx";`);
324

325
    return done(db);
2✔
326
};
327

328
postgresModule.createIndices = function (callback) {
5✔
329
    if (!postgresModule.pool) {
1!
UNCOV
330
        winston.warn('[database/createIndices] database not initialized');
×
UNCOV
331
        return callback();
×
332
    }
333

334
    const query = postgresModule.pool.query.bind(postgresModule.pool);
1✔
335

336
    winston.info('[database] Checking database indices.');
1✔
337
    async.series([
1✔
338
        async.apply(query, `CREATE INDEX IF NOT EXISTS "idx__legacy_zset__key__score" ON "legacy_zset"("_key" ASC, "score" DESC)`),
339
        async.apply(query, `CREATE INDEX IF NOT EXISTS "idx__legacy_object__expireAt" ON "legacy_object"("expireAt" ASC)`),
340
    ], (err) => {
341
        if (err) {
1!
NEW
342
            winston.error(`Error creating index ${err.message}`);
×
NEW
343
            return callback(err);
×
344
        }
345
        winston.info('[database] Checking database indices done!');
1✔
346
        callback();
1✔
347
    });
348
};
349

350
postgresModule.checkCompatibility = function (callback) {
5✔
351
    const postgresPkg = require('pg/package.json');
1✔
352
    postgresModule.checkCompatibilityVersion(postgresPkg.version, callback);
1✔
353
};
354

355
postgresModule.checkCompatibilityVersion = function (version, callback) {
5✔
356
    if (semver.lt(version, '7.0.0')) {
2✔
357
        return callback(new Error('The `pg` package is out-of-date, please run `./nodebb setup` again.'));
1✔
358
    }
359

360
    callback();
1✔
361
};
362

363
postgresModule.info = async function (db) {
5✔
364
    if (!db) {
3✔
365
        db = await connection.connect(nconf.get('postgres'));
1✔
366
    }
367
    postgresModule.pool = postgresModule.pool || db;
3!
368
    const res = await db.query(`
3✔
369
        SELECT true "postgres",
370
           current_setting('server_version') "version",
371
             EXTRACT(EPOCH FROM NOW() - pg_postmaster_start_time()) * 1000 "uptime"
372
    `);
373
    return {
3✔
374
        ...res.rows[0],
375
        raw: JSON.stringify(res.rows[0], null, 4),
376
    };
377
};
378

379
postgresModule.close = async function () {
5✔
380
    await postgresModule.pool.end();
4✔
381
};
382

383
require('./postgres/main')(postgresModule);
5✔
384
require('./postgres/hash')(postgresModule);
5✔
385
require('./postgres/sets')(postgresModule);
5✔
386
require('./postgres/sorted')(postgresModule);
5✔
387
require('./postgres/list')(postgresModule);
5✔
388
require('./postgres/transaction')(postgresModule);
5✔
389

390
require('../promisify')(postgresModule, ['client', 'sessionStore', 'pool', 'transaction']);
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc