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 = []) { const nestedPaths = excludePaths.filter( (p) => p.startsWith(`${column}.`) && !p.includes("[]") ); const arrayPaths = excludePaths.filter( (p) => p.startsWith(`${column}.`) && p.includes("[]") ); // ─── Group array paths ────────────────────────────────────────────────────── // Separate bare array strips → "personal_info.addresses[]" // from key-level strips → "personal_info.addresses[].city" const bareArrayStrips = new Set(); // fields to remove entirely e.g. "addresses" const arrayGroups = {}; // fields to partially strip e.g. { addresses: ["city"] } for (const path of arrayPaths) { const stripped = path.replace(`${column}.`, ""); // "addresses[].city" or "addresses[]" if (stripped.endsWith("[]")) { // ─── Bare array strip — remove the whole field ───────────────────────── const arrayField = stripped.replace("[]", ""); bareArrayStrips.add(arrayField); } else { // ─── Key-level strip — remove specific keys inside each element ──────── const [arrayPart, key] = stripped.split("[]."); // ["addresses", "city"] const arrayField = arrayPart.replace("[]", ""); // Skip if the whole array is already being stripped entirely if (bareArrayStrips.has(arrayField)) continue; if (!arrayGroups[arrayField]) arrayGroups[arrayField] = []; arrayGroups[arrayField].push(key); } } if (!nestedPaths.length && !bareArrayStrips.size && !Object.keys(arrayGroups).length) return null; let literal = `"${column}"`; // ─── 1. Strip nested keys ─────────────────────────────────────────────────── for (const path of nestedPaths) { const keys = path.replace(`${column}.`, "").split("."); literal = `(${literal} #- '{${keys.join(",")}}')`; } // ─── 2. Strip entire array fields ────────────────────────────────────────── for (const arrayField of bareArrayStrips) { literal = `(${literal} #- '{${arrayField}}')`; } // ─── 3. Strip specific keys inside array elements ────────────────────────── 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 };