/** * Anonymizes a Firebase index JSON file by replacing collection and field names * @param {string} jsonString - Prettified Firebase index JSON string * @returns {{anonymizedJson: string, nameMapping: Object}} - Anonymized JSON and mapping */ function anonymizeFirebaseIndexes(jsonString) { // Parse the JSON const indexData = JSON.parse(jsonString); // Maps to track name replacements const collectionMapping = new Map(); const collectionFieldMapping = new Map(); // Maps collection to its field mappings let collectionCounter = 1; // Helper to get or create anonymized collection name function getAnonymizedCollection(originalName) { if (!collectionMapping.has(originalName)) { collectionMapping.set(originalName, `collection${collectionCounter}`); collectionFieldMapping.set(originalName, new Map()); collectionCounter++; } return collectionMapping.get(originalName); } // Helper to get or create anonymized field name for a specific collection function getAnonymizedField(collectionName, fieldName) { // Ensure collection exists in mapping if (!collectionFieldMapping.has(collectionName)) { collectionFieldMapping.set(collectionName, new Map()); } const fieldMap = collectionFieldMapping.get(collectionName); if (!fieldMap.has(fieldName)) { const fieldCounter = fieldMap.size + 1; let anonymizedName = `field${fieldCounter}`; // Preserve special suffixes if (fieldName.toLowerCase().includes('timestamp')) { anonymizedName += '_timestamp'; } fieldMap.set(fieldName, anonymizedName); } return fieldMap.get(fieldName); } // Deep clone the object to avoid modifying the original const anonymizedData = JSON.parse(JSON.stringify(indexData)); // Process indexes if (anonymizedData.indexes && Array.isArray(anonymizedData.indexes)) { anonymizedData.indexes.forEach(index => { const originalCollection = index.collectionGroup; // Anonymize collectionGroup if (index.collectionGroup) { index.collectionGroup = getAnonymizedCollection(index.collectionGroup); } // Anonymize fields if (index.fields && Array.isArray(index.fields)) { index.fields.forEach(field => { if (field.fieldPath && originalCollection) { field.fieldPath = getAnonymizedField(originalCollection, field.fieldPath); } }); } }); } // Process fieldOverrides if (anonymizedData.fieldOverrides && Array.isArray(anonymizedData.fieldOverrides)) { anonymizedData.fieldOverrides.forEach(override => { const originalCollection = override.collectionGroup; // Anonymize collectionGroup if (override.collectionGroup) { override.collectionGroup = getAnonymizedCollection(override.collectionGroup); } // Anonymize fieldPath if (override.fieldPath && originalCollection) { override.fieldPath = getAnonymizedField(originalCollection, override.fieldPath); } }); } // Verification functions function generateIndexSignature(index) { const collection = index.collectionGroup || ''; const queryScope = index.queryScope || ''; const fields = (index.fields || []) .map(f => `${f.fieldPath}:${f.order || ''}`) .sort() .join(','); return `${collection}|${queryScope}|${fields}`; } function generateFieldOverrideSignature(override) { const collection = override.collectionGroup || ''; const field = override.fieldPath || ''; const ttl = override.ttl !== undefined ? override.ttl : ''; const indexes = (override.indexes || []) .map(idx => `${idx.queryScope || ''}:${idx.order || ''}`) .sort() .join(','); return `${collection}|${field}|${ttl}|${indexes}`; } // Verify correctness const verification = { original: { indexCount: indexData.indexes ? indexData.indexes.length : 0, fieldOverrideCount: indexData.fieldOverrides ? indexData.fieldOverrides.length : 0, uniqueCollections: new Set(), uniqueFields: new Set(), indexSignatures: new Set(), fieldOverrideSignatures: new Set() }, anonymized: { indexCount: anonymizedData.indexes ? anonymizedData.indexes.length : 0, fieldOverrideCount: anonymizedData.fieldOverrides ? anonymizedData.fieldOverrides.length : 0, uniqueCollections: new Set(), uniqueFields: new Set(), indexSignatures: new Set(), fieldOverrideSignatures: new Set() } }; // Collect original metrics if (indexData.indexes) { indexData.indexes.forEach(index => { if (index.collectionGroup) { verification.original.uniqueCollections.add(index.collectionGroup); } if (index.fields) { index.fields.forEach(field => { if (field.fieldPath) { verification.original.uniqueFields.add(field.fieldPath); } }); } verification.original.indexSignatures.add(generateIndexSignature(index)); }); } if (indexData.fieldOverrides) { indexData.fieldOverrides.forEach(override => { if (override.collectionGroup) { verification.original.uniqueCollections.add(override.collectionGroup); } if (override.fieldPath) { verification.original.uniqueFields.add(override.fieldPath); } verification.original.fieldOverrideSignatures.add(generateFieldOverrideSignature(override)); }); } // Collect anonymized metrics if (anonymizedData.indexes) { anonymizedData.indexes.forEach(index => { if (index.collectionGroup) { verification.anonymized.uniqueCollections.add(index.collectionGroup); } if (index.fields) { index.fields.forEach(field => { if (field.fieldPath) { verification.anonymized.uniqueFields.add(field.fieldPath); } }); } verification.anonymized.indexSignatures.add(generateIndexSignature(index)); }); } if (anonymizedData.fieldOverrides) { anonymizedData.fieldOverrides.forEach(override => { if (override.collectionGroup) { verification.anonymized.uniqueCollections.add(override.collectionGroup); } if (override.fieldPath) { verification.anonymized.uniqueFields.add(override.fieldPath); } verification.anonymized.fieldOverrideSignatures.add(generateFieldOverrideSignature(override)); }); } // Create mapping object with collection-field associations const nameMapping = { collections: {} }; // Build the mapping structure collectionMapping.forEach((anonymizedName, originalName) => { nameMapping.collections[originalName] = { name: anonymizedName, fields: {} }; const fieldMap = collectionFieldMapping.get(originalName); if (fieldMap) { fieldMap.forEach((anonymizedField, originalField) => { nameMapping.collections[originalName].fields[originalField] = anonymizedField; }); } }); // Add verification info const verificationReport = { isValid: true, details: [] }; // Check index counts if (verification.original.indexCount !== verification.anonymized.indexCount) { verificationReport.isValid = false; verificationReport.details.push( `Index count mismatch: original=${verification.original.indexCount}, anonymized=${verification.anonymized.indexCount}` ); } else { verificationReport.details.push( `✓ Index count matches: ${verification.original.indexCount}` ); } // Check field override counts if (verification.original.fieldOverrideCount !== verification.anonymized.fieldOverrideCount) { verificationReport.isValid = false; verificationReport.details.push( `Field override count mismatch: original=${verification.original.fieldOverrideCount}, anonymized=${verification.anonymized.fieldOverrideCount}` ); } else { verificationReport.details.push( `✓ Field override count matches: ${verification.original.fieldOverrideCount}` ); } // Check unique counts if (verification.original.uniqueCollections.size !== verification.anonymized.uniqueCollections.size) { verificationReport.isValid = false; verificationReport.details.push( `Unique collection count mismatch: original=${verification.original.uniqueCollections.size}, anonymized=${verification.anonymized.uniqueCollections.size}` ); } else { verificationReport.details.push( `✓ Unique collection count matches: ${verification.original.uniqueCollections.size}` ); } // Note: We don't check unique field count globally anymore since fields are now unique per collection verificationReport.details.push( `✓ Fields are now unique per collection (not globally)` ); // Check index signatures if (verification.original.indexSignatures.size !== verification.anonymized.indexSignatures.size) { verificationReport.isValid = false; verificationReport.details.push( `Index signature count mismatch: original=${verification.original.indexSignatures.size}, anonymized=${verification.anonymized.indexSignatures.size}` ); } else { verificationReport.details.push( `✓ Index signature count matches: ${verification.original.indexSignatures.size}` ); } // Check field override signatures if (verification.original.fieldOverrideSignatures.size !== verification.anonymized.fieldOverrideSignatures.size) { verificationReport.isValid = false; verificationReport.details.push( `Field override signature count mismatch: original=${verification.original.fieldOverrideSignatures.size}, anonymized=${verification.anonymized.fieldOverrideSignatures.size}` ); } else { verificationReport.details.push( `✓ Field override signature count matches: ${verification.original.fieldOverrideSignatures.size}` ); } // Log verification results console.log('Verification Report:'); console.log('==================='); verificationReport.details.forEach(detail => console.log(detail)); console.log(`Overall validation: ${verificationReport.isValid ? 'PASSED ✓' : 'FAILED ✗'}`); return { anonymizedJson: JSON.stringify(anonymizedData, null, 2), nameMapping: nameMapping, verification: verificationReport }; } // Read from stdin when run as a script if (require.main === module) { let inputData = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', (chunk) => { inputData += chunk; }); process.stdin.on('end', () => { try { const result = anonymizeFirebaseIndexes(inputData); console.log('\nAnonymized JSON:'); console.log('================'); console.log(result.anonymizedJson); console.log('\nName Mapping:'); console.log('============='); console.log(JSON.stringify(result.nameMapping, null, 2)); } catch (error) { console.error('Error processing input:', error.message); process.exit(1); } }); // Handle no input timeout setTimeout(() => { if (inputData === '') { console.error('No input received. Please pipe a Firebase index JSON file to this script.'); console.error('Example: cat firestore.indexes.json | node anonymize-firebase-indexes.js'); process.exit(1); } }, 100); } // Export for use as a module if (typeof module !== 'undefined' && module.exports) { module.exports = anonymizeFirebaseIndexes; }