@rapidrabbit/gitdb
Version:
A powerful, flexible database module for storing data in various formats with local file and GitHub storage options
1,199 lines (1,018 loc) • 32.6 kB
JavaScript
/**
* GitDB - Core Database Class
* Main database interface that orchestrates storage and format operations
*/
const StorageManager = require('../storage/StorageManager')
const FormatManager = require('../formats/FormatManager')
const { LockManager } = require('../utils/LockManager')
const { EncryptionManager } = require('../utils/EncryptionManager')
const { BackupManager } = require('../utils/BackupManager')
const { ValidationManager } = require('../utils/ValidationManager')
const HistoryManager = require('../utils/HistoryManager')
class GitDB {
constructor (options = {}) {
// Core configuration
this.storageType = options.storageType || 'local'
this.format = options.format || 'json'
// Initialize managers
this.storage = new StorageManager(options)
this.formatManager = new FormatManager(options)
this.lockManager = new LockManager(options)
this.encryptionManager = new EncryptionManager(options)
this.backupManager = new BackupManager(options)
this.validationManager = new ValidationManager(options)
// History manager (only for GitHub storage)
this.historyManager = null
// Performance tracking
this.metrics = {
operations: 0,
totalTime: 0,
errors: 0,
cacheHits: 0,
cacheMisses: 0
}
// Cache for performance optimization
this.cache = new Map()
this.cacheTimeout = options.cacheTimeout || 300000 // 5 minutes
}
/**
* Initialize the database
*/
async initialize () {
try {
await this.storage.initialize()
await this.formatManager.initialize()
await this.lockManager.initialize()
await this.encryptionManager.initialize()
// Set storage reference for backup manager
this.backupManager.setStorage(this.storage)
await this.backupManager.initialize()
// Initialize history manager for GitHub storage
if (this.storageType === 'github' && this.storage.storage && this.storage.storage.octokit) {
this.historyManager = new HistoryManager(this.storage.storage)
}
return true
} catch (error) {
this.metrics.errors++
throw new Error(`Failed to initialize GitDB: ${error.message}`)
}
}
/**
* CREATE - Add a new item to the database
*/
async create (collection, item, commitMessage = null) {
const startTime = Date.now()
this.metrics.operations++
try {
await this.lockManager.acquireLock()
const data = await this.readDataFile()
if (!data[collection]) {
data[collection] = []
}
// Generate unique ID if not provided
if (!item.id) {
item.id = this.generateId()
}
// Add timestamps
item.createdAt = new Date().toISOString()
item.updatedAt = new Date().toISOString()
// Validate item
this.validationManager.validateItem(item)
data[collection].push(item)
const message = commitMessage || `Add item to ${collection}: ${item.id}`
await this.writeDataFile(data, message)
this.metrics.totalTime += Date.now() - startTime
return item
} catch (error) {
this.metrics.errors++
throw error
} finally {
await this.lockManager.releaseLock()
}
}
/**
* READ - Get items from the database with advanced filtering
*/
async read (collection, query = {}) {
const startTime = Date.now()
this.metrics.operations++
try {
const cacheKey = `${collection}-${JSON.stringify(query)}`
// Check cache first
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey)
if (Date.now() - cached.timestamp < this.cacheTimeout) {
this.metrics.cacheHits++
this.metrics.totalTime += Date.now() - startTime
return cached.data
}
}
this.metrics.cacheMisses++
const data = await this.readDataFile()
if (!data[collection]) {
return []
}
let items = data[collection]
// Apply filters
if (Object.keys(query).length > 0) {
items = items.filter(item => {
return Object.entries(query).every(([key, value]) => {
if (typeof value === 'function') {
return value(item[key])
}
return item[key] === value
})
})
}
// Cache the result
this.cache.set(cacheKey, { data: items, timestamp: Date.now() })
this.metrics.totalTime += Date.now() - startTime
return items
} catch (error) {
this.metrics.errors++
throw error
}
}
/**
* READ BY ID - Get a specific item by ID
*/
async readById (collection, id) {
const items = await this.read(collection, { id })
return items.length > 0 ? items[0] : null
}
/**
* UPDATE - Update an existing item
*/
async update (collection, id, updates, commitMessage = null) {
const startTime = Date.now()
this.metrics.operations++
try {
await this.lockManager.acquireLock()
const data = await this.readDataFile()
if (!data[collection]) {
throw new Error(`Collection '${collection}' not found`)
}
const itemIndex = data[collection].findIndex(item => item.id === id)
if (itemIndex === -1) {
throw new Error(`Item with id '${id}' not found in collection '${collection}'`)
}
// Preserve original data and add updates
const originalItem = data[collection][itemIndex]
const updatedItem = {
...originalItem,
...updates,
id: originalItem.id, // Prevent ID changes
createdAt: originalItem.createdAt, // Preserve creation time
updatedAt: new Date().toISOString()
}
// Validate updated item
this.validationManager.validateItem(updatedItem)
data[collection][itemIndex] = updatedItem
const message = commitMessage || `Update item in ${collection}: ${id}`
await this.writeDataFile(data, message)
// Clear cache for this collection
this.clearCache(collection)
this.metrics.totalTime += Date.now() - startTime
return updatedItem
} catch (error) {
this.metrics.errors++
throw error
} finally {
await this.lockManager.releaseLock()
}
}
/**
* DELETE - Remove an item from the database
*/
async delete (collection, id, commitMessage = null) {
const startTime = Date.now()
this.metrics.operations++
try {
await this.lockManager.acquireLock()
const data = await this.readDataFile()
if (!data[collection]) {
throw new Error(`Collection '${collection}' not found`)
}
const itemIndex = data[collection].findIndex(item => item.id === id)
if (itemIndex === -1) {
throw new Error(`Item with id '${id}' not found in collection '${collection}'`)
}
const deletedItem = data[collection][itemIndex]
data[collection].splice(itemIndex, 1)
const message = commitMessage || `Delete item from ${collection}: ${id}`
await this.writeDataFile(data, message)
// Clear cache for this collection
this.clearCache(collection)
this.metrics.totalTime += Date.now() - startTime
return deletedItem
} catch (error) {
this.metrics.errors++
throw error
} finally {
await this.lockManager.releaseLock()
}
}
/**
* DELETE COLLECTION - Remove an entire collection
*/
async deleteCollection (collection, commitMessage = null) {
const startTime = Date.now()
this.metrics.operations++
try {
await this.lockManager.acquireLock()
const data = await this.readDataFile()
if (!data[collection]) {
throw new Error(`Collection '${collection}' not found`)
}
const deletedCollection = data[collection]
delete data[collection]
const message = commitMessage || `Delete collection: ${collection}`
await this.writeDataFile(data, message)
// Clear cache for this collection
this.clearCache(collection)
this.metrics.totalTime += Date.now() - startTime
return deletedCollection
} catch (error) {
this.metrics.errors++
throw error
} finally {
await this.lockManager.releaseLock()
}
}
/**
* BULK OPERATIONS - Perform multiple operations atomically
*/
async bulkOperation (operations, commitMessage = null) {
const startTime = Date.now()
this.metrics.operations++
try {
await this.lockManager.acquireLock()
const data = await this.readDataFile()
const results = []
for (const operation of operations) {
const { type, collection, ...params } = operation
switch (type) {
case 'create': {
if (!data[collection]) {
data[collection] = []
}
const newItem = {
...params.item,
id: params.item.id || this.generateId(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
this.validationManager.validateItem(newItem)
data[collection].push(newItem)
results.push({ type: 'create', success: true, item: newItem })
break
}
case 'update': {
if (!data[collection]) {
results.push({ type: 'update', success: false, error: 'Collection not found' })
continue
}
const itemIndex = data[collection].findIndex(item => item.id === params.id)
if (itemIndex === -1) {
results.push({ type: 'update', success: false, error: 'Item not found' })
continue
}
const originalItem = data[collection][itemIndex]
const updatedItem = {
...originalItem,
...params.updates,
id: originalItem.id,
createdAt: originalItem.createdAt,
updatedAt: new Date().toISOString()
}
this.validationManager.validateItem(updatedItem)
data[collection][itemIndex] = updatedItem
results.push({ type: 'update', success: true, item: updatedItem })
break
}
case 'delete': {
if (!data[collection]) {
results.push({ type: 'delete', success: false, error: 'Collection not found' })
continue
}
const deleteIndex = data[collection].findIndex(item => item.id === params.id)
if (deleteIndex === -1) {
results.push({ type: 'delete', success: false, error: 'Item not found' })
continue
}
const deletedItem = data[collection][deleteIndex]
data[collection].splice(deleteIndex, 1)
results.push({ type: 'delete', success: true, item: deletedItem })
break
}
default:
results.push({ type, success: false, error: 'Unknown operation type' })
}
}
const message = commitMessage || `Bulk operations: ${operations.length} operations`
await this.writeDataFile(data, message)
// Clear cache for affected collections
const affectedCollections = [...new Set(operations.map(op => op.collection))]
affectedCollections.forEach(collection => this.clearCache(collection))
this.metrics.totalTime += Date.now() - startTime
return results
} catch (error) {
this.metrics.errors++
throw error
} finally {
await this.lockManager.releaseLock()
}
}
// ===== ADVANCED FILTERING METHODS =====
/**
* MAP - Transform items in a collection
*/
async map (collection, mapper) {
const items = await this.read(collection)
return items.map(mapper)
}
/**
* REDUCE - Reduce items in a collection to a single value
*/
async reduce (collection, reducer, initialValue) {
const items = await this.read(collection)
return items.reduce(reducer, initialValue)
}
/**
* FILTER - Filter items using a predicate function
*/
async filter (collection, predicate) {
const items = await this.read(collection)
return items.filter(predicate)
}
/**
* FIND - Find first item matching predicate
*/
async find (collection, predicate) {
const items = await this.read(collection)
return items.find(predicate)
}
/**
* FIND ALL - Find all items matching predicate
*/
async findAll (collection, predicate) {
const items = await this.read(collection)
return items.filter(predicate)
}
/**
* SORT - Sort items by a key or function
*/
async sort (collection, sortBy, order = 'asc') {
const items = await this.read(collection)
if (typeof sortBy === 'function') {
return items.sort((a, b) => {
const result = sortBy(a, b)
return order === 'desc' ? -result : result
})
}
return items.sort((a, b) => {
let aVal = a[sortBy]
let bVal = b[sortBy]
// Handle different data types
if (typeof aVal === 'string' && typeof bVal === 'string') {
aVal = aVal.toLowerCase()
bVal = bVal.toLowerCase()
}
if (aVal < bVal) return order === 'desc' ? 1 : -1
if (aVal > bVal) return order === 'desc' ? -1 : 1
return 0
})
}
/**
* LIMIT - Limit the number of results
*/
async limit (collection, limit, offset = 0) {
const items = await this.read(collection)
return items.slice(offset, offset + limit)
}
/**
* GROUP BY - Group items by a key
*/
async groupBy (collection, key) {
const items = await this.read(collection)
return items.reduce((groups, item) => {
const groupKey = item[key]
if (!groups[groupKey]) {
groups[groupKey] = []
}
groups[groupKey].push(item)
return groups
}, {})
}
// ===== AGGREGATION METHODS =====
/**
* COUNT - Count items in a collection
*/
async count (collection, predicate = null) {
const items = await this.read(collection)
if (predicate) {
return items.filter(predicate).length
}
return items.length
}
/**
* SUM - Sum values in a collection
*/
async sum (collection, key) {
const items = await this.read(collection)
return items.reduce((sum, item) => {
const value = parseFloat(item[key]) || 0
return sum + value
}, 0)
}
/**
* AVERAGE - Calculate average of values in a collection
*/
async average (collection, key) {
const items = await this.read(collection)
if (items.length === 0) return 0
const sum = await this.sum(collection, key)
return sum / items.length
}
/**
* MIN - Find minimum value in a collection
*/
async min (collection, key) {
const items = await this.read(collection)
if (items.length === 0) return null
return items.reduce((min, item) => {
const value = parseFloat(item[key])
if (isNaN(value)) return min
return min === null || value < min ? value : min
}, null)
}
/**
* MAX - Find maximum value in a collection
*/
async max (collection, key) {
const items = await this.read(collection)
if (items.length === 0) return null
return items.reduce((max, item) => {
const value = parseFloat(item[key])
if (isNaN(value)) return max
return max === null || value > max ? value : max
}, null)
}
/**
* DISTINCT - Get distinct values for a key
*/
async distinct (collection, key) {
const items = await this.read(collection)
const values = items.map(item => item[key])
return [...new Set(values)]
}
// ===== SQL-LIKE QUERY METHODS =====
/**
* SELECT - SQL-like select with conditions
*/
async select (collection, options = {}) {
let items = await this.read(collection)
// WHERE clause
if (options.where) {
items = items.filter(item => this.evaluateWhereClause(item, options.where))
}
// ORDER BY clause
if (options.orderBy) {
const [key, order] = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy, 'asc']
items = await this.sort(collection, key, order)
}
// LIMIT clause
if (options.limit) {
const offset = options.offset || 0
items = items.slice(offset, offset + options.limit)
}
// SELECT specific fields
if (options.fields) {
items = items.map(item => {
const selected = {}
options.fields.forEach(field => {
if (Object.prototype.hasOwnProperty.call(item, field)) {
selected[field] = item[field]
}
})
return selected
})
}
return items
}
/**
* WHERE - Complex where conditions
*/
async where (collection, conditions) {
const items = await this.read(collection)
return items.filter(item => this.evaluateWhereClause(item, conditions))
}
/**
* Evaluate WHERE clause conditions
*/
evaluateWhereClause (item, conditions) {
if (typeof conditions === 'function') {
return conditions(item)
}
if (typeof conditions === 'object') {
return Object.entries(conditions).every(([key, condition]) => {
if (typeof condition === 'object' && condition.operator) {
return this.evaluateOperator(item[key], condition.operator, condition.value)
}
return item[key] === condition
})
}
return true
}
/**
* Evaluate comparison operators
*/
evaluateOperator (value, operator, compareValue) {
switch (operator.toLowerCase()) {
case 'eq':
case '=':
return value === compareValue
case 'ne':
case '!=':
return value !== compareValue
case 'gt':
case '>':
return value > compareValue
case 'gte':
case '>=':
return value >= compareValue
case 'lt':
case '<':
return value < compareValue
case 'lte':
case '<=':
return value <= compareValue
case 'in':
return Array.isArray(compareValue) && compareValue.includes(value)
case 'nin':
case 'not in':
return Array.isArray(compareValue) && !compareValue.includes(value)
case 'like':
return typeof value === 'string' &&
typeof compareValue === 'string' &&
value.toLowerCase().includes(compareValue.toLowerCase())
case 'regex':
return typeof value === 'string' &&
typeof compareValue === 'string' &&
new RegExp(compareValue).test(value)
default:
return false
}
}
/**
* JOIN - Join two collections
*/
async join (collection1, collection2, joinKey1, joinKey2, joinType = 'inner') {
const items1 = await this.read(collection1)
const items2 = await this.read(collection2)
const result = []
items1.forEach(item1 => {
const matchingItems2 = items2.filter(item2 => item1[joinKey1] === item2[joinKey2])
if (matchingItems2.length > 0) {
matchingItems2.forEach(item2 => {
result.push({
...item1,
[collection2]: item2
})
})
} else if (joinType === 'left') {
result.push({
...item1,
[collection2]: null
})
}
})
if (joinType === 'right') {
items2.forEach(item2 => {
const matchingItems1 = items1.filter(item1 => item1[joinKey1] === item2[joinKey2])
if (matchingItems1.length === 0) {
result.push({
[collection1]: null,
...item2
})
}
})
}
return result
}
/**
* AGGREGATE - Perform aggregation operations
*/
async aggregate (collection, pipeline) {
let items = await this.read(collection)
for (const stage of pipeline) {
switch (stage.type) {
case 'match':
items = items.filter(item => this.evaluateWhereClause(item, stage.conditions))
break
case 'group':
items = this.aggregateGroup(items, stage)
break
case 'sort':
items = items.sort((a, b) => {
for (const [key, order] of Object.entries(stage.sort)) {
const aVal = a[key]
const bVal = b[key]
if (aVal < bVal) return order === 1 ? -1 : 1
if (aVal > bVal) return order === 1 ? 1 : -1
}
return 0
})
break
case 'limit':
items = items.slice(0, stage.limit)
break
case 'project':
items = items.map(item => {
const projected = {}
Object.entries(stage.fields).forEach(([key, value]) => {
if (value === 1) {
projected[key] = item[key]
} else if (typeof value === 'string') {
projected[key] = item[value]
}
})
return projected
})
break
}
}
return items
}
/**
* Helper for aggregate group operations
*/
aggregateGroup (items, stage) {
const groups = {}
items.forEach(item => {
const groupKey = stage.groupBy.reduce((key, field) => {
return key + '_' + (item[field] || 'null')
}, '')
if (!groups[groupKey]) {
groups[groupKey] = {}
stage.groupBy.forEach(field => {
groups[groupKey][field] = item[field]
})
}
stage.aggregations.forEach(agg => {
const field = agg.field
const value = parseFloat(item[field]) || 0
if (!groups[groupKey][agg.name]) {
groups[groupKey][agg.name] = agg.type === 'sum' || agg.type === 'avg'
? 0
: agg.type === 'min'
? Infinity
: agg.type === 'max' ? -Infinity : []
}
switch (agg.type) {
case 'sum':
groups[groupKey][agg.name] += value
break
case 'avg':
groups[groupKey][agg.name] += value
break
case 'min':
groups[groupKey][agg.name] = Math.min(groups[groupKey][agg.name], value)
break
case 'max':
groups[groupKey][agg.name] = Math.max(groups[groupKey][agg.name], value)
break
case 'count':
groups[groupKey][agg.name]++
break
case 'push':
groups[groupKey][agg.name].push(item[field])
break
}
})
})
// Calculate averages
Object.values(groups).forEach(group => {
stage.aggregations.forEach(agg => {
if (agg.type === 'avg' && group[agg.name] !== undefined) {
group[agg.name] = group[agg.name] / group.count
}
})
})
return Object.values(groups)
}
// ===== UTILITY METHODS =====
/**
* Read data file from storage
*/
async readDataFile () {
const rawData = await this.storage.read()
const parsedData = this.formatManager.parse(rawData)
// Decrypt if encryption is enabled
if (this.encryptionManager.isEnabled()) {
return this.encryptionManager.decrypt(parsedData)
}
return parsedData
}
/**
* Write data file to storage
*/
async writeDataFile (data, commitMessage) {
// Encrypt if encryption is enabled
let dataToWrite = data
if (this.encryptionManager.isEnabled()) {
dataToWrite = this.encryptionManager.encrypt(data)
}
const serializedData = this.formatManager.serialize(dataToWrite)
await this.storage.write(serializedData, commitMessage)
// Create backup if enabled
if (this.backupManager.isEnabled()) {
await this.backupManager.createBackup(data)
}
}
/**
* Generate unique ID
*/
generateId () {
const crypto = require('crypto')
return crypto.randomUUID()
}
/**
* Clear cache for a collection
*/
clearCache (collection) {
for (const [key] of this.cache) {
if (key.startsWith(`${collection}-`)) {
this.cache.delete(key)
}
}
}
/**
* Get database statistics
*/
async getStats () {
const data = await this.readDataFile()
const stats = {
format: this.format,
storageType: this.storageType,
collections: Object.keys(data).length,
totalItems: 0,
collectionStats: {},
performance: {
...this.metrics,
averageTime: this.metrics.operations > 0
? this.metrics.totalTime / this.metrics.operations
: 0,
cacheHitRate: (this.metrics.cacheHits + this.metrics.cacheMisses) > 0
? this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)
: 0
}
}
for (const [collection, items] of Object.entries(data)) {
stats.collectionStats[collection] = items.length
stats.totalItems += items.length
}
return stats
}
/**
* Get performance metrics
*/
getMetrics () {
return {
...this.metrics,
averageTime: this.metrics.operations > 0
? this.metrics.totalTime / this.metrics.operations
: 0,
cacheHitRate: (this.metrics.cacheHits + this.metrics.cacheMisses) > 0
? this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses)
: 0
}
}
/**
* Reset performance metrics
*/
resetMetrics () {
this.metrics = {
operations: 0,
totalTime: 0,
errors: 0,
cacheHits: 0,
cacheMisses: 0
}
}
/**
* Export database to a local file
*/
async export (exportPath) {
const data = await this.readDataFile()
const serializedData = this.formatManager.serialize(data)
const fs = require('fs').promises
await fs.writeFile(exportPath, serializedData)
return exportPath
}
/**
* Import database from a local file
*/
async import (importPath, commitMessage = 'Import data') {
await this.lockManager.acquireLock()
try {
const fs = require('fs').promises
const importData = await fs.readFile(importPath, 'utf8')
const data = this.formatManager.parse(importData)
await this.writeDataFile(data, commitMessage)
// Clear all cache
this.cache.clear()
return true
} finally {
await this.lockManager.releaseLock()
}
}
/**
* Convert data to a different format
*/
async convertFormat (newFormat, options = {}) {
const data = await this.readDataFile()
// Create new instance with different format
const newDb = new GitDB({
...this.getConfig(),
format: newFormat,
...options
})
await newDb.initialize()
await newDb.writeDataFile(data, 'Convert format')
return newDb
}
/**
* Get current configuration
*/
getConfig () {
return {
storageType: this.storageType,
format: this.format,
...this.storage.getConfig(),
...this.formatManager.getConfig(),
...this.lockManager.getConfig(),
...this.encryptionManager.getConfig(),
...this.backupManager.getConfig(),
cacheTimeout: this.cacheTimeout
}
}
// ===== HISTORY METHODS (GitHub Storage Only) =====
/**
* Check if history tracking is available
*/
hasHistoryTracking () {
return this.historyManager !== null
}
/**
* Get complete commit history for the data file
*/
async getCommitHistory (limit = 50) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.getFileHistory(limit)
}
/**
* Get the content of the data file at a specific commit
*/
async getFileAtCommit (sha) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.getFileAtCommit(sha)
}
/**
* Get the difference between two commits
*/
async getDiff (sha1, sha2) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.getDiff(sha1, sha2)
}
/**
* Get the history of a specific record by ID
*/
async getRecordHistory (collection, recordId, limit = 50) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.getRecordHistory(collection, recordId, limit)
}
/**
* Search commit history by various criteria
*/
async searchHistory (options = {}) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.searchHistory(options)
}
/**
* Get statistics about the data file history
*/
async getHistoryStats () {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.getHistoryStats()
}
/**
* Get the timeline of changes for a collection
*/
async getCollectionTimeline (collection, limit = 50) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
return await this.historyManager.getCollectionTimeline(collection, limit)
}
/**
* Revert the data file to a specific commit
*/
async revertToCommit (sha, commitMessage = null) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
const message = commitMessage || `Revert to commit ${sha}`
const result = await this.historyManager.revertToCommit(sha, message)
// Clear cache after revert
this.cache.clear()
return result
}
/**
* Get the version history of a specific field in a record
*/
async getFieldHistory (collection, recordId, fieldName, limit = 50) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
const recordHistory = await this.getRecordHistory(collection, recordId, limit)
return recordHistory.map(entry => ({
commit: entry.commit,
message: entry.message,
author: entry.author,
date: entry.date,
action: entry.action,
fieldValue: entry.record[fieldName],
previousValue: entry.previousRecord ? entry.previousRecord[fieldName] : null
}))
}
/**
* Get all records that were created, updated, or deleted in a specific commit
*/
async getCommitChanges (sha) {
if (!this.hasHistoryTracking()) {
throw new Error('History tracking is only available for GitHub storage')
}
try {
const fileData = await this.getFileAtCommit(sha)
const data = JSON.parse(fileData.content)
// Get the previous commit to compare
const commits = await this.getCommitHistory(2)
if (commits.length < 2) {
return { added: data, modified: {}, deleted: {} }
}
const previousFileData = await this.getFileAtCommit(commits[1].sha)
const previousData = JSON.parse(previousFileData.content)
const changes = {
added: {},
modified: {},
deleted: {}
}
// Compare collections
const allCollections = new Set([
...Object.keys(data),
...Object.keys(previousData)
])
for (const collection of allCollections) {
const currentItems = data[collection] || []
const previousItems = previousData[collection] || []
const currentIds = new Set(currentItems.map(item => item.id))
const previousIds = new Set(previousItems.map(item => item.id))
// Find added items
changes.added[collection] = currentItems.filter(item => !previousIds.has(item.id))
// Find deleted items
changes.deleted[collection] = previousItems.filter(item => !currentIds.has(item.id))
// Find modified items
changes.modified[collection] = currentItems.filter(item => {
if (!previousIds.has(item.id)) return false
const previousItem = previousItems.find(p => p.id === item.id)
return JSON.stringify(item) !== JSON.stringify(previousItem)
})
}
return changes
} catch (error) {
throw new Error(`Failed to get commit changes: ${error.message}`)
}
}
/**
* Close database connections
*/
async close () {
await this.lockManager.releaseLock()
await this.storage.close()
this.cache.clear()
}
}
module.exports = GitDB