NodeBB/NodeBB

View on GitHub
src/database/postgres/hash.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict';

module.exports = function (module) {
    const helpers = require('./helpers');

    module.setObject = async function (key, data) {
        if (!key || !data) {
            return;
        }

        if (data.hasOwnProperty('')) {
            delete data[''];
        }

        await module.transaction(async (client) => {
            const dataString = JSON.stringify(data);
            async function setOne(key) {
                await helpers.ensureLegacyObjectType(client, key, 'hash');
                await client.query({
                    name: 'setObject',
                    text: `
    INSERT INTO "legacy_hash" ("_key", "data")
    VALUES ($1::TEXT, $2::TEXT::JSONB)
    ON CONFLICT ("_key")
    DO UPDATE SET "data" = "legacy_hash"."data" || $2::TEXT::JSONB`,
                    values: [key, dataString],
                });
            }
            if (Array.isArray(key)) {
                await Promise.all(key.map(k => setOne(k)));
            } else {
                await setOne(key);
            }
        });
    };

    module.setObjectBulk = async function (keys, data) {
        if (!keys.length || !data.length) {
            return;
        }
        // TODO: single query?
        await Promise.all(keys.map((k, i) => module.setObject(k, data[i])));
    };

    module.setObjectField = async function (key, field, value) {
        if (!field) {
            return;
        }

        await module.transaction(async (client) => {
            const valueString = JSON.stringify(value);
            async function setOne(key) {
                await helpers.ensureLegacyObjectType(client, key, 'hash');
                await client.query({
                    name: 'setObjectField',
                    text: `
    INSERT INTO "legacy_hash" ("_key", "data")
    VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::TEXT::JSONB))
    ON CONFLICT ("_key")
    DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], $3::TEXT::JSONB)`,
                    values: [key, field, valueString],
                });
            }

            if (Array.isArray(key)) {
                await Promise.all(key.map(k => setOne(k)));
            } else {
                await setOne(key);
            }
        });
    };

    module.getObject = async function (key, fields = []) {
        if (!key) {
            return null;
        }
        if (fields.length) {
            return await module.getObjectFields(key, fields);
        }
        const res = await module.pool.query({
            name: 'getObject',
            text: `
SELECT h."data"
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
            values: [key],
        });

        return res.rows.length ? res.rows[0].data : null;
    };

    module.getObjects = async function (keys, fields = []) {
        if (!Array.isArray(keys) || !keys.length) {
            return [];
        }
        if (fields.length) {
            return await module.getObjectsFields(keys, fields);
        }
        const res = await module.pool.query({
            name: 'getObjects',
            text: `
SELECT h."data"
  FROM UNNEST($1::TEXT[]) WITH ORDINALITY k("_key", i)
  LEFT OUTER JOIN "legacy_object_live" o
               ON o."_key" = k."_key"
  LEFT OUTER JOIN "legacy_hash" h
               ON o."_key" = h."_key"
              AND o."type" = h."type"
 ORDER BY k.i ASC`,
            values: [keys],
        });

        return res.rows.map(row => row.data);
    };

    module.getObjectField = async function (key, field) {
        if (!key) {
            return null;
        }

        const res = await module.pool.query({
            name: 'getObjectField',
            text: `
SELECT h."data"->>$2::TEXT f
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
            values: [key, field],
        });

        return res.rows.length ? res.rows[0].f : null;
    };

    module.getObjectFields = async function (key, fields) {
        if (!key) {
            return null;
        }
        if (!Array.isArray(fields) || !fields.length) {
            return await module.getObject(key);
        }
        const res = await module.pool.query({
            name: 'getObjectFields',
            text: `
SELECT (SELECT jsonb_object_agg(f, d."value")
          FROM UNNEST($2::TEXT[]) f
          LEFT OUTER JOIN jsonb_each(h."data") d
                       ON d."key" = f) d
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT`,
            values: [key, fields],
        });

        if (res.rows.length) {
            return res.rows[0].d;
        }

        const obj = {};
        fields.forEach((f) => {
            obj[f] = null;
        });

        return obj;
    };

    module.getObjectsFields = async function (keys, fields) {
        if (!Array.isArray(keys) || !keys.length) {
            return [];
        }

        if (!Array.isArray(fields) || !fields.length) {
            return await module.getObjects(keys);
        }
        const res = await module.pool.query({
            name: 'getObjectsFields',
            text: `
SELECT (SELECT jsonb_object_agg(f, d."value")
          FROM UNNEST($2::TEXT[]) f
          LEFT OUTER JOIN jsonb_each(h."data") d
                       ON d."key" = f) d
  FROM UNNEST($1::text[]) WITH ORDINALITY k("_key", i)
  LEFT OUTER JOIN "legacy_object_live" o
               ON o."_key" = k."_key"
  LEFT OUTER JOIN "legacy_hash" h
               ON o."_key" = h."_key"
              AND o."type" = h."type"
 ORDER BY k.i ASC`,
            values: [keys, fields],
        });

        return res.rows.map(row => row.d);
    };

    module.getObjectKeys = async function (key) {
        if (!key) {
            return;
        }

        const res = await module.pool.query({
            name: 'getObjectKeys',
            text: `
SELECT ARRAY(SELECT jsonb_object_keys(h."data")) k
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
            values: [key],
        });

        return res.rows.length ? res.rows[0].k : [];
    };

    module.getObjectValues = async function (key) {
        const data = await module.getObject(key);
        return data ? Object.values(data) : [];
    };

    module.isObjectField = async function (key, field) {
        if (!key) {
            return;
        }

        const res = await module.pool.query({
            name: 'isObjectField',
            text: `
SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b
  FROM "legacy_object_live" o
 INNER JOIN "legacy_hash" h
         ON o."_key" = h."_key"
        AND o."type" = h."type"
 WHERE o."_key" = $1::TEXT
 LIMIT 1`,
            values: [key, field],
        });

        return res.rows.length ? res.rows[0].b : false;
    };

    module.isObjectFields = async function (key, fields) {
        if (!key) {
            return;
        }

        const data = await module.getObjectFields(key, fields);
        if (!data) {
            return fields.map(() => false);
        }
        return fields.map(field => data.hasOwnProperty(field) && data[field] !== null);
    };

    module.deleteObjectField = async function (key, field) {
        await module.deleteObjectFields(key, [field]);
    };

    module.deleteObjectFields = async function (key, fields) {
        if (!key || (Array.isArray(key) && !key.length) || !Array.isArray(fields) || !fields.length) {
            return;
        }
        async function delKey(key, fields) {
            await module.pool.query({
                name: 'deleteObjectFields',
                text: `
    UPDATE "legacy_hash"
       SET "data" = COALESCE((SELECT jsonb_object_agg("key", "value")
                                FROM jsonb_each("data")
                               WHERE "key" <> ALL ($2::TEXT[])), '{}')
     WHERE "_key" = $1::TEXT`,
                values: [key, fields],
            });
        }
        if (Array.isArray(key)) {
            await Promise.all(key.map(k => delKey(k, fields)));
        } else {
            await delKey(key, fields);
        }
    };

    module.incrObjectField = async function (key, field) {
        return await module.incrObjectFieldBy(key, field, 1);
    };

    module.decrObjectField = async function (key, field) {
        return await module.incrObjectFieldBy(key, field, -1);
    };

    module.incrObjectFieldBy = async function (key, field, value) {
        value = parseInt(value, 10);

        if (!key || isNaN(value)) {
            return null;
        }

        return await module.transaction(async (client) => {
            if (Array.isArray(key)) {
                await helpers.ensureLegacyObjectsType(client, key, 'hash');
            } else {
                await helpers.ensureLegacyObjectType(client, key, 'hash');
            }

            const res = await client.query(Array.isArray(key) ? {
                name: 'incrObjectFieldByMulti',
                text: `
INSERT INTO "legacy_hash" ("_key", "data")
SELECT UNNEST($1::TEXT[]), jsonb_build_object($2::TEXT, $3::NUMERIC)
ON CONFLICT ("_key")
DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC))
RETURNING ("data"->>$2::TEXT)::NUMERIC v`,
                values: [key, field, value],
            } : {
                name: 'incrObjectFieldBy',
                text: `
INSERT INTO "legacy_hash" ("_key", "data")
VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::NUMERIC))
ON CONFLICT ("_key")
DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC))
RETURNING ("data"->>$2::TEXT)::NUMERIC v`,
                values: [key, field, value],
            });
            return Array.isArray(key) ? res.rows.map(r => parseFloat(r.v)) : parseFloat(res.rows[0].v);
        });
    };
};