Files
starr-philproperties/utils/excludeJSONBPaths.js
T
2026-05-05 23:18:47 +08:00

73 lines
2.6 KiB
JavaScript

const { Sequelize } = require('sequelize')
/**
* Strips a key from every element in a JSONB array using PostgreSQL's
* jsonb_agg + #- operator in a subquery.
*
* @param {string} column - JSONB column e.g. "personal_info"
* @param {string} arrayField - array field name e.g. "addresses"
* @param {string[]} keys - keys to strip from each array element e.g. ["street", "zip"]
* @returns {string} SQL fragment
*/
function buildArrayStrip(column, arrayField, keys) {
const keyRemovals = keys.reduce(
(acc, key) => `(${acc} #- '{${key}}')`,
"elem"
);
return `(
SELECT jsonb_agg(${keyRemovals})
FROM jsonb_array_elements("${column}"->'${arrayField}') AS elem
)`;
}
/**
* Builds a Sequelize literal that strips JSONB paths at the DB level.
* Supports:
* - nested keys: "personal_info.name.given_name"
* - array item keys: "personal_info.addresses[].street"
*
* @param {string} column - JSONB column name e.g. "personal_info"
* @param {string[]} excludePaths - dot-notation paths
* @returns {Array|null} Sequelize literal attribute tuple
*/
function excludeJsonbPaths(column, excludePaths = []) {
// Separate nested paths from array paths
const nestedPaths = excludePaths.filter(
(p) => p.startsWith(`${column}.`) && !p.includes("[]")
);
const arrayPaths = excludePaths.filter(
(p) => p.startsWith(`${column}.`) && p.includes("[]")
);
// Group array paths by their field name
// e.g. { addresses: ["street", "zip"], phone_number: ["country_code"] }
const arrayGroups = {};
for (const path of arrayPaths) {
const stripped = path.replace(`${column}.`, ""); // addresses[].street
const [arrayField, key] = stripped.split("[]."); // ["addresses", "street"]
if (!arrayGroups[arrayField]) arrayGroups[arrayField] = [];
arrayGroups[arrayField].push(key);
}
if (!nestedPaths.length && !Object.keys(arrayGroups).length) return null;
// Start with the column and chain #- for nested paths
let literal = `"${column}"`;
// Strip nested keys first
for (const path of nestedPaths) {
const keys = path.replace(`${column}.`, "").split(".");
literal = `(${literal} #- '{${keys.join(",")}}')`;
}
// Then rebuild array fields with stripped keys using jsonb_set
for (const [arrayField, keys] of Object.entries(arrayGroups)) {
const arrayStrip = buildArrayStrip(column, arrayField, keys);
literal = `jsonb_set(${literal}, '{${arrayField}}', COALESCE(${arrayStrip}, '[]'))`;
}
return [Sequelize.literal(literal), column];
}
module.exports = { excludeJsonbPaths };