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 };