db-data-gd-uploader 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ import mongoose from 'mongoose'
2
+ import { readdir } from 'node:fs/promises'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+
5
+ const DEFAULT_EXCLUDED_MODELS = new Set(['system.indexes'])
6
+ const EXCLUDED_SYSTEM_COLLECTIONS = new Set(['system.indexes', 'system.profile', 'system.views'])
7
+
8
+ function serializeValue(value) {
9
+ if (value === null || value === undefined) return value
10
+ if (value instanceof mongoose.Types.ObjectId) return value.toString()
11
+ if (value instanceof Date) return value.toISOString()
12
+ if (Buffer.isBuffer(value)) return value.toString('base64')
13
+ if (Array.isArray(value)) return value.map(serializeValue)
14
+
15
+ if (typeof value === 'object') {
16
+ return Object.fromEntries(
17
+ Object.entries(value).map(([key, nestedValue]) => [key, serializeValue(nestedValue)]),
18
+ )
19
+ }
20
+
21
+ return value
22
+ }
23
+
24
+ async function importModelFiles(modelsDirectory) {
25
+ try {
26
+ const modelFiles = await readdir(modelsDirectory)
27
+
28
+ await Promise.all(
29
+ modelFiles
30
+ .filter((file) => /\.(js|mjs)$/.test(file))
31
+ .map((file) => import(pathToFileURL(`${modelsDirectory}/${file}`).href)),
32
+ )
33
+ } catch (error) {
34
+ if (error.code !== 'ENOENT') throw error
35
+ }
36
+ }
37
+
38
+ export async function extractAllModels({
39
+ modelsDirectory = fileURLToPath(new URL('../models/', import.meta.url)),
40
+ excludedModels = DEFAULT_EXCLUDED_MODELS,
41
+ modelNames = [],
42
+ queryOptions = {},
43
+ } = {}) {
44
+ await importModelFiles(modelsDirectory)
45
+
46
+ let selectedModels = mongoose.modelNames()
47
+
48
+ if (Array.isArray(modelNames) && modelNames.length > 0) {
49
+ selectedModels = selectedModels.filter((name) => modelNames.includes(name))
50
+ }
51
+
52
+ selectedModels = selectedModels.filter((name) => !excludedModels.has(name))
53
+
54
+ const extractedData = []
55
+
56
+ for (const modelName of selectedModels) {
57
+ const model = mongoose.model(modelName)
58
+ const documents = await model.find({}, null, queryOptions).lean().exec()
59
+
60
+ extractedData.push({
61
+ modelName,
62
+ collectionName: model.collection.name,
63
+ data: documents.map(serializeValue),
64
+ })
65
+ }
66
+
67
+ return extractedData
68
+ }
69
+
70
+ /**
71
+ * Extract data from all collections across all databases in MongoDB
72
+ * @param {string} mongoUri - MongoDB connection URI
73
+ * @param {Array<string>} excludedDatabases - Databases to exclude from extraction
74
+ * @returns {Promise<Array>} Array of objects containing database name, collection name, and documents
75
+ */
76
+ export async function extractAllDatabasesCollections(
77
+ mongoUri = process.env.MONGODB_URI,
78
+ excludedDatabases = ['admin', 'config', 'local'],
79
+ ) {
80
+ const adminDb = mongoose.connection.getClient().db('admin')
81
+ const databases = await adminDb.admin().listDatabases()
82
+
83
+ const extractedData = []
84
+
85
+ for (const dbInfo of databases.databases) {
86
+ const dbName = dbInfo.name
87
+
88
+ // Skip excluded databases
89
+ if (excludedDatabases.includes(dbName)) {
90
+ continue
91
+ }
92
+
93
+ try {
94
+ const db = mongoose.connection.getClient().db(dbName)
95
+ const collections = await db.listCollections().toArray()
96
+
97
+ for (const collectionInfo of collections) {
98
+ const collectionName = collectionInfo.name
99
+
100
+ // Skip system collections
101
+ if (EXCLUDED_SYSTEM_COLLECTIONS.has(collectionName)) {
102
+ continue
103
+ }
104
+
105
+ const collection = db.collection(collectionName)
106
+ const documents = await collection.find({}).toArray()
107
+
108
+ extractedData.push({
109
+ database: dbName,
110
+ collectionName,
111
+ documentCount: documents.length,
112
+ data: documents.map(serializeValue),
113
+ })
114
+ }
115
+ } catch (error) {
116
+ console.error(`Error extracting data from database ${dbName}:`, error.message)
117
+ extractedData.push({
118
+ database: dbName,
119
+ error: error.message,
120
+ collectionName: null,
121
+ data: [],
122
+ })
123
+ }
124
+ }
125
+
126
+ return extractedData
127
+ }
128
+
129
+ /**
130
+ * Extract data from specific databases only
131
+ * @param {string} mongoUri - MongoDB connection URI
132
+ * @param {Array<string>} databaseNames - Specific databases to extract from
133
+ * @returns {Promise<Array>} Array of objects containing database name, collection name, and documents
134
+ */
135
+ export async function extractSpecificDatabases(mongoUri = process.env.MONGODB_URI, databaseNames = []) {
136
+ const extractedData = []
137
+
138
+ for (const dbName of databaseNames) {
139
+ try {
140
+ const db = mongoose.connection.getClient().db(dbName)
141
+ const collections = await db.listCollections().toArray()
142
+
143
+ for (const collectionInfo of collections) {
144
+ const collectionName = collectionInfo.name
145
+
146
+ // Skip system collections
147
+ if (EXCLUDED_SYSTEM_COLLECTIONS.has(collectionName)) {
148
+ continue
149
+ }
150
+
151
+ const collection = db.collection(collectionName)
152
+ const documents = await collection.find({}).toArray()
153
+
154
+ extractedData.push({
155
+ database: dbName,
156
+ collectionName,
157
+ documentCount: documents.length,
158
+ data: documents.map(serializeValue),
159
+ })
160
+ }
161
+ } catch (error) {
162
+ console.error(`Error extracting data from database ${dbName}:`, error.message)
163
+ extractedData.push({
164
+ database: dbName,
165
+ error: error.message,
166
+ collectionName: null,
167
+ data: [],
168
+ })
169
+ }
170
+ }
171
+
172
+ return extractedData
173
+ }
174
+
175
+ export { serializeValue }
@@ -0,0 +1,153 @@
1
+ import ExcelJS from 'exceljs'
2
+
3
+ function flattenObject(value, prefix = '') {
4
+ if (value === null || value === undefined) return { [prefix || 'value']: '' }
5
+ if (Array.isArray(value)) return { [prefix || 'value']: JSON.stringify(value) }
6
+ if (typeof value !== 'object' || value instanceof Date) return { [prefix || 'value']: value }
7
+
8
+ return Object.entries(value).reduce(
9
+ (result, [key, nestedValue]) => ({
10
+ ...result,
11
+ ...flattenObject(nestedValue, prefix ? `${prefix}.${key}` : key),
12
+ }),
13
+ {},
14
+ )
15
+ }
16
+
17
+ function normalizeCellValue(value) {
18
+ if (value === null || value === undefined) return ''
19
+ if (typeof value === 'object' && !(value instanceof Date)) return JSON.stringify(value)
20
+ return value
21
+ }
22
+
23
+ export async function createExcelWorkbook(extractedData, { workbookName = 'database-export' } = {}) {
24
+ if (!Array.isArray(extractedData)) throw new TypeError('extractedData must be an array')
25
+
26
+ const workbook = new ExcelJS.Workbook()
27
+ workbook.creator = 'GD Uploader - Complete Backup'
28
+ workbook.created = new Date()
29
+
30
+ // Group collections by database
31
+ const groupedByDatabase = {}
32
+ for (const item of extractedData) {
33
+ if (!groupedByDatabase[item.database]) {
34
+ groupedByDatabase[item.database] = []
35
+ }
36
+ groupedByDatabase[item.database].push(item)
37
+ }
38
+
39
+ // Create one sheet per database
40
+ for (const [database, collections] of Object.entries(groupedByDatabase)) {
41
+ const worksheet = workbook.addWorksheet(database.slice(0, 31))
42
+
43
+ if (collections.length === 0) continue
44
+
45
+ // Row 1: Database name (merged header)
46
+ const totalColumns = collections.length * 2
47
+ worksheet.mergeCells(1, 1, 1, totalColumns)
48
+ const dbHeaderCell = worksheet.getCell(1, 1)
49
+ dbHeaderCell.value = database
50
+ dbHeaderCell.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } }
51
+ dbHeaderCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1F4E78' } }
52
+ dbHeaderCell.alignment = { horizontal: 'center', vertical: 'center' }
53
+ worksheet.getRow(1).height = 25
54
+
55
+ // Row 2: Collection names
56
+ let colIdx = 1
57
+ for (const collection of collections) {
58
+ const rows = Array.isArray(collection.data) ? collection.data : []
59
+ worksheet.mergeCells(2, colIdx, 2, colIdx + 1)
60
+ const collCell = worksheet.getCell(2, colIdx)
61
+ collCell.value = `${collection.collectionName} (${rows.length})`
62
+ collCell.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } }
63
+ collCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } }
64
+ collCell.alignment = { horizontal: 'center', vertical: 'center' }
65
+ colIdx += 2
66
+ }
67
+ worksheet.getRow(2).height = 20
68
+
69
+ // Row 3: Key/Value headers
70
+ colIdx = 1
71
+ for (const _collection of collections) {
72
+ worksheet.getCell(3, colIdx).value = 'key'
73
+ worksheet.getCell(3, colIdx + 1).value = 'value'
74
+ for (let i = colIdx; i <= colIdx + 1; i++) {
75
+ const cell = worksheet.getCell(3, i)
76
+ cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }
77
+ cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF70AD47' } }
78
+ cell.alignment = { horizontal: 'center', vertical: 'center' }
79
+ }
80
+ colIdx += 2
81
+ }
82
+ worksheet.getRow(3).height = 18
83
+
84
+ // Calculate max rows
85
+ const maxRows = Math.max(...collections.map(c => (Array.isArray(c.data) ? c.data.length : 0)), 0)
86
+
87
+ // Data rows - each document gets multiple rows (one per key-value pair)
88
+ let currentExcelRow = 4
89
+
90
+ for (let docIdx = 0; docIdx < maxRows; docIdx++) {
91
+ // Find max fields needed for this document index across all collections
92
+ let maxFieldsThisDoc = 0
93
+ for (const collection of collections) {
94
+ const rows = Array.isArray(collection.data) ? collection.data : []
95
+ if (docIdx < rows.length) {
96
+ const flatDoc = flattenObject(rows[docIdx])
97
+ maxFieldsThisDoc = Math.max(maxFieldsThisDoc, Object.keys(flatDoc).length)
98
+ }
99
+ }
100
+
101
+ // Create rows for each field
102
+ for (let fieldIdx = 0; fieldIdx < maxFieldsThisDoc; fieldIdx++) {
103
+ colIdx = 1
104
+
105
+ for (const collection of collections) {
106
+ const rows = Array.isArray(collection.data) ? collection.data : []
107
+
108
+ if (docIdx < rows.length) {
109
+ const flatDoc = flattenObject(rows[docIdx])
110
+ const entries = Object.entries(flatDoc)
111
+
112
+ if (fieldIdx < entries.length) {
113
+ const [key, value] = entries[fieldIdx]
114
+ worksheet.getCell(currentExcelRow, colIdx).value = key
115
+ worksheet.getCell(currentExcelRow, colIdx + 1).value = normalizeCellValue(value)
116
+ }
117
+ }
118
+
119
+ colIdx += 2
120
+ }
121
+
122
+ currentExcelRow++
123
+ }
124
+ }
125
+
126
+ // Set column widths
127
+ for (let col = 1; col <= totalColumns; col++) {
128
+ worksheet.getColumn(col).width = 22
129
+ }
130
+
131
+ // Add borders to all cells
132
+ for (let row = 1; row < currentExcelRow; row++) {
133
+ for (let col = 1; col <= totalColumns; col++) {
134
+ const cell = worksheet.getCell(row, col)
135
+ cell.border = {
136
+ top: { style: 'thin' },
137
+ left: { style: 'thin' },
138
+ bottom: { style: 'thin' },
139
+ right: { style: 'thin' },
140
+ }
141
+ cell.alignment = { wrapText: true, vertical: 'top' }
142
+ }
143
+ }
144
+ }
145
+
146
+ if (workbook.worksheets.length === 0) workbook.addWorksheet('No data')
147
+ const buffer = await workbook.xlsx.writeBuffer()
148
+ return {
149
+ buffer: Buffer.from(buffer),
150
+ fileName: `${workbookName.replace(/[^a-z0-9-_]/gi, '-').toLowerCase()}.xlsx`,
151
+ contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
152
+ }
153
+ }