arcadiasuite/server/migration/bson-parser.ts

329 lines
8.2 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
interface BsonDocument {
[key: string]: any;
}
interface ParseResult {
collection: string;
count: number;
fields: string[];
sample: BsonDocument | null;
documents: BsonDocument[];
}
interface AnalysisResult {
sourceType: string;
sourceSystem: string;
totalCollections: number;
totalRecords: number;
collections: {
name: string;
count: number;
fields: string[];
sampleData: BsonDocument | null;
}[];
}
function decodeBsonElement(data: Buffer, offset: number): { key: string | null; value: any; newOffset: number } {
if (offset >= data.length) {
return { key: null, value: null, newOffset: offset };
}
const elemType = data[offset];
offset += 1;
const keyEnd = data.indexOf(0x00, offset);
if (keyEnd === -1) {
return { key: null, value: null, newOffset: data.length };
}
const key = data.slice(offset, keyEnd).toString('utf-8');
offset = keyEnd + 1;
let value: any = null;
switch (elemType) {
case 0x01: // double
value = data.readDoubleLE(offset);
offset += 8;
break;
case 0x02: // string
const strLen = data.readInt32LE(offset);
offset += 4;
value = data.slice(offset, offset + strLen - 1).toString('utf-8');
offset += strLen;
break;
case 0x03: // document
const docLen = data.readInt32LE(offset);
const { doc } = decodeBsonDoc(data, offset);
value = doc;
offset += docLen;
break;
case 0x04: // array
const arrLen = data.readInt32LE(offset);
const { doc: arrDoc } = decodeBsonDoc(data, offset);
value = arrDoc ? Object.values(arrDoc) : [];
offset += arrLen;
break;
case 0x05: // binary
const binLen = data.readInt32LE(offset);
offset += 5 + binLen;
value = '<binary>';
break;
case 0x07: // ObjectId
value = data.slice(offset, offset + 12).toString('hex');
offset += 12;
break;
case 0x08: // boolean
value = data[offset] !== 0;
offset += 1;
break;
case 0x09: // datetime
const ts = Number(data.readBigInt64LE(offset));
try {
value = new Date(ts).toISOString();
} catch {
value = `timestamp:${ts}`;
}
offset += 8;
break;
case 0x0A: // null
value = null;
break;
case 0x10: // int32
value = data.readInt32LE(offset);
offset += 4;
break;
case 0x11: // timestamp
offset += 8;
value = '<timestamp>';
break;
case 0x12: // int64
value = Number(data.readBigInt64LE(offset));
offset += 8;
break;
case 0x13: // decimal128
offset += 16;
value = '<decimal128>';
break;
default:
value = `<type:${elemType.toString(16)}>`;
}
return { key, value, newOffset: offset };
}
function decodeBsonDoc(data: Buffer, start: number = 0): { doc: BsonDocument | null; newOffset: number } {
if (start + 4 > data.length) {
return { doc: null, newOffset: start };
}
const docLen = data.readInt32LE(start);
if (docLen <= 4 || start + docLen > data.length) {
return { doc: null, newOffset: start };
}
const doc: BsonDocument = {};
let offset = start + 4;
const end = start + docLen - 1;
while (offset < end) {
const { key, value, newOffset } = decodeBsonElement(data, offset);
if (key === null) break;
doc[key] = value;
offset = newOffset;
}
return { doc, newOffset: start + docLen };
}
export function parseBsonFile(filepath: string): BsonDocument[] {
const data = fs.readFileSync(filepath);
if (data.length === 0) return [];
const docs: BsonDocument[] = [];
let offset = 0;
while (offset < data.length - 4) {
const { doc, newOffset } = decodeBsonDoc(data, offset);
if (doc === null || newOffset <= offset) break;
docs.push(doc);
offset = newOffset;
}
return docs;
}
export function analyzeBackupDirectory(dirPath: string): AnalysisResult {
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.bson'));
const collections: AnalysisResult['collections'] = [];
let totalRecords = 0;
for (const file of files) {
const collectionName = file.replace('.bson', '');
const filepath = path.join(dirPath, file);
try {
const docs = parseBsonFile(filepath);
const count = docs.length;
totalRecords += count;
if (count > 0) {
const allFields = new Set<string>();
docs.slice(0, 50).forEach(doc => {
Object.keys(doc).forEach(k => allFields.add(k));
});
collections.push({
name: collectionName,
count,
fields: Array.from(allFields).sort(),
sampleData: docs[0] || null
});
}
} catch (error) {
console.error(`Error parsing ${file}:`, error);
}
}
collections.sort((a, b) => b.count - a.count);
return {
sourceType: 'mongodb',
sourceSystem: 'Sistema Legado',
totalCollections: collections.length,
totalRecords,
collections
};
}
export function getCollectionDocuments(dirPath: string, collectionName: string, limit?: number): BsonDocument[] {
const filepath = path.join(dirPath, `${collectionName}.bson`);
if (!fs.existsSync(filepath)) {
throw new Error(`Collection ${collectionName} not found`);
}
const docs = parseBsonFile(filepath);
return limit ? docs.slice(0, limit) : docs;
}
export const defaultMappings: Record<string, { target: string; fields: Record<string, string> }> = {
DtoPessoa: {
target: 'customers',
fields: {
'_id': 'legacy_id',
'NomeFantasia': 'name',
'RazaoSocial': 'legal_name',
'CNPJ_CPF': 'cpf_cnpj',
'IE': 'state_registration',
'IM': 'municipal_registration',
'Email': 'email',
'Telefone': 'phone',
'Celular': 'mobile',
'Logradouro': 'street',
'Numero': 'number',
'Complemento': 'complement',
'Bairro': 'neighborhood',
'Cidade': 'city',
'UF': 'state',
'CEP': 'zip_code',
'PessoaFisica': 'is_individual',
'Observacoes': 'notes'
}
},
DtoProduto: {
target: 'products',
fields: {
'_id': 'legacy_id',
'Nome': 'name',
'Codigo': 'sku',
'CodigoNFe': 'code',
'Descricao': 'description',
'PrecoVenda': 'sell_price',
'PrecoCusto': 'cost_price',
'NCM_NFe': 'ncm',
'CEST_NFe': 'cest',
'Categoria': 'category',
'Marca': 'brand',
'Unidade': 'unit',
'Ativo': 'is_active',
'ControlaEstoque': 'track_inventory'
}
},
DtoVenda: {
target: 'pos_sales',
fields: {
'_id': 'legacy_id',
'Codigo': 'sale_number',
'Data': 'created_at',
'ValorTotal': 'total_amount',
'ValorDesconto': 'discount_amount',
'Status': 'status',
'FormaPagamento': 'payment_method',
'Observacoes': 'notes'
}
},
DtoLancamento: {
target: 'fin_transactions',
fields: {
'_id': 'legacy_id',
'Descricao': 'description',
'Entrada': 'entrada',
'Saida': 'saida',
'DataVencimento': 'due_date',
'DataPagamento': 'payment_date',
'Pago': 'is_paid',
'ValorPago': 'paid_amount',
'Cliente': 'customer_name',
'FormaPagamento': 'payment_method',
'PlanoDeConta': 'category',
'Observacoes': 'notes'
}
},
DtoColaborador: {
target: 'retail_sellers',
fields: {
'_id': 'legacy_id',
'Nome': 'name',
'Codigo': 'code',
'CPF': 'cpf',
'Email': 'email',
'Telefone': 'phone',
'Tipo': 'role'
}
},
DtoEmpresa: {
target: 'retail_stores',
fields: {
'_id': 'legacy_id',
'NomeFantasia': 'name',
'RazaoSocial': 'legal_name',
'CNPJ': 'cnpj',
'InscricaoEstadual': 'state_registration',
'Logradouro': 'street',
'Numero': 'number',
'Complemento': 'complement',
'Cidade': 'city',
'UF': 'state',
'CEP': 'zip_code',
'Email': 'email',
'Telefone': 'phone'
}
},
DtoCase: {
target: 'service_orders',
fields: {
'_id': 'legacy_id',
'Nome': 'order_number',
'DataCadastro': 'created_at',
'Descricao': 'description',
'Cliente': 'customer_name',
'Tipo': 'service_type',
'Status': 'status'
}
}
};