- MongoDB storage: cwmp_exchanges collection with indexes (deviceId+timestamp, deviceId+sequenceNumber unique, TTL 24h) - ExchangeStoreService: CWMP method extraction, per-device queue for ordering, retry logic, message cap (5000/device) - Exchange API: GET/DELETE/SSE stream endpoints with Last-Event-ID reconnection - CwmpLoggerMiddleware: dual-write (MongoDB + legacy files) with fire-and-forget - Frontend: SequenceViewer (CPE↔ACS lifelines, arrows, auto-scroll, virtual scroll) + InspectorPanel (formatted XML, copy, syntax highlighting) - Tr069Trace page: integrated new viewer with col-6/col-6 layout - DeviceDetail page: collapsible exchange section when device is traced - 14 property-based tests, 40+ unit tests, 5 integration tests - Health check includes index verification - Config: feature flags, SSE timeouts, retry policy
553 lines
18 KiB
TypeScript
553 lines
18 KiB
TypeScript
/**
|
|
* Database seeder for cwmp_exchanges collection.
|
|
*
|
|
* Generates realistic CWMP exchange data for integration/manual testing.
|
|
* Can be run standalone via: npx tsx database/seeders/cwmp_exchanges_seeder.ts
|
|
*
|
|
* Configuration via environment variables:
|
|
* SEED_DEVICE_COUNT - Number of devices to generate (default: 5)
|
|
* SEED_EXCHANGES_MIN - Minimum exchanges per device (default: 20)
|
|
* SEED_EXCHANGES_MAX - Maximum exchanges per device (default: 100)
|
|
* MONGODB_URI - MongoDB connection string
|
|
* MONGODB_DB_NAME - Database name (default: adam-acs)
|
|
*/
|
|
|
|
import { MongoClient, type Db } from 'mongodb'
|
|
|
|
// ─── Configuration ──────────────────────────────────────────────────────────
|
|
|
|
interface SeederConfig {
|
|
deviceCount: number
|
|
exchangesMin: number
|
|
exchangesMax: number
|
|
mongoUri: string
|
|
dbName: string
|
|
}
|
|
|
|
function getConfig(): SeederConfig {
|
|
return {
|
|
deviceCount: Number(process.env.SEED_DEVICE_COUNT ?? 5),
|
|
exchangesMin: Number(process.env.SEED_EXCHANGES_MIN ?? 20),
|
|
exchangesMax: Number(process.env.SEED_EXCHANGES_MAX ?? 100),
|
|
mongoUri: process.env.MONGODB_URI ?? 'mongodb://localhost:27017',
|
|
dbName: process.env.MONGODB_DB_NAME ?? 'adam-acs',
|
|
}
|
|
}
|
|
|
|
// ─── CWMP Method Templates ──────────────────────────────────────────────────
|
|
|
|
type Direction = 'CPE→ACS' | 'ACS→CPE'
|
|
|
|
interface ExchangeTemplate {
|
|
cwmpMethod: string
|
|
direction: Direction
|
|
xmlGenerator: () => string
|
|
}
|
|
|
|
const MANUFACTURERS = ['Sagemcom', 'Huawei', 'ZTE', 'Nokia', 'Technicolor']
|
|
const PRODUCT_CLASSES = ['Livebox 4', 'BM632w', 'ZXHN H298A', 'G-240W-A', 'TG789vac']
|
|
const OUI_LIST = ['F8B7FF', '202BC1', '001D08', '00E0FC', '74BD7B']
|
|
|
|
function randomInt(min: number, max: number): number {
|
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
|
}
|
|
|
|
function randomChoice<T>(arr: T[]): T {
|
|
return arr[randomInt(0, arr.length - 1)]
|
|
}
|
|
|
|
function generateDeviceId(index: number): string {
|
|
const oui = OUI_LIST[index % OUI_LIST.length]
|
|
const serial = `DEVICE-${String(index + 1).padStart(3, '0')}`
|
|
return `${oui}-${PRODUCT_CLASSES[index % PRODUCT_CLASSES.length]}-${serial}`
|
|
}
|
|
|
|
function generateSessionId(): string {
|
|
const srcIp = `10.0.${randomInt(0, 255)}.${randomInt(1, 254)}`
|
|
const srcPort = randomInt(30000, 65000)
|
|
return `${srcIp}:${srcPort}->192.168.1.1:8080`
|
|
}
|
|
|
|
// ─── XML Body Generators ────────────────────────────────────────────────────
|
|
|
|
function wrapSoapEnvelope(body: string): string {
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/"
|
|
xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/"
|
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
|
|
<soap-env:Header>
|
|
<cwmp:ID soap-env:mustUnderstand="1">${randomInt(100000, 999999)}</cwmp:ID>
|
|
</soap-env:Header>
|
|
<soap-env:Body>
|
|
${body}
|
|
</soap-env:Body>
|
|
</soap-env:Envelope>`
|
|
}
|
|
|
|
function generateInformXml(deviceIndex: number): string {
|
|
const oui = OUI_LIST[deviceIndex % OUI_LIST.length]
|
|
const manufacturer = MANUFACTURERS[deviceIndex % MANUFACTURERS.length]
|
|
const productClass = PRODUCT_CLASSES[deviceIndex % PRODUCT_CLASSES.length]
|
|
const serial = `DEVICE-${String(deviceIndex + 1).padStart(3, '0')}`
|
|
const events = randomChoice([
|
|
['0 BOOTSTRAP', '1 BOOT'],
|
|
['2 PERIODIC'],
|
|
['1 BOOT'],
|
|
['6 CONNECTION REQUEST'],
|
|
])
|
|
|
|
const eventString = events
|
|
.map(
|
|
(e, i) =>
|
|
`<EventStruct><EventCode>${e}</EventCode><CommandKey>${i}</CommandKey></EventStruct>`
|
|
)
|
|
.join('\n ')
|
|
|
|
return wrapSoapEnvelope(`<cwmp:Inform>
|
|
<DeviceId>
|
|
<Manufacturer>${manufacturer}</Manufacturer>
|
|
<OUI>${oui}</OUI>
|
|
<ProductClass>${productClass}</ProductClass>
|
|
<SerialNumber>${serial}</SerialNumber>
|
|
</DeviceId>
|
|
<Event soap-enc:arrayType="cwmp:EventStruct[${events.length}]">
|
|
${eventString}
|
|
</Event>
|
|
<MaxEnvelopes>1</MaxEnvelopes>
|
|
<CurrentTime>${new Date().toISOString()}</CurrentTime>
|
|
<RetryCount>0</RetryCount>
|
|
<ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[4]">
|
|
<ParameterValueStruct>
|
|
<Name>Device.DeviceInfo.HardwareVersion</Name>
|
|
<Value xsi:type="xsd:string">v2.1</Value>
|
|
</ParameterValueStruct>
|
|
<ParameterValueStruct>
|
|
<Name>Device.DeviceInfo.SoftwareVersion</Name>
|
|
<Value xsi:type="xsd:string">SR40_sip-fr-4.62.0</Value>
|
|
</ParameterValueStruct>
|
|
<ParameterValueStruct>
|
|
<Name>Device.ManagementServer.ConnectionRequestURL</Name>
|
|
<Value xsi:type="xsd:string">http://10.0.0.${randomInt(1, 254)}:7547/</Value>
|
|
</ParameterValueStruct>
|
|
<ParameterValueStruct>
|
|
<Name>Device.ManagementServer.ParameterKey</Name>
|
|
<Value xsi:type="xsd:string"></Value>
|
|
</ParameterValueStruct>
|
|
</ParameterList>
|
|
</cwmp:Inform>`)
|
|
}
|
|
|
|
function generateInformResponseXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:InformResponse>
|
|
<MaxEnvelopes>1</MaxEnvelopes>
|
|
</cwmp:InformResponse>`)
|
|
}
|
|
|
|
function generateGetParameterValuesXml(paths: string[]): string {
|
|
const paramNames = paths.map((p) => `<string>${p}</string>`).join('\n ')
|
|
return wrapSoapEnvelope(`<cwmp:GetParameterValues>
|
|
<ParameterNames soap-enc:arrayType="xsd:string[${paths.length}]">
|
|
${paramNames}
|
|
</ParameterNames>
|
|
</cwmp:GetParameterValues>`)
|
|
}
|
|
|
|
function generateGetParameterValuesResponseXml(paths: string[]): string {
|
|
const params = paths
|
|
.map(
|
|
(p) => `<ParameterValueStruct>
|
|
<Name>${p}</Name>
|
|
<Value xsi:type="xsd:string">value_${randomInt(1, 9999)}</Value>
|
|
</ParameterValueStruct>`
|
|
)
|
|
.join('\n ')
|
|
return wrapSoapEnvelope(`<cwmp:GetParameterValuesResponse>
|
|
<ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[${paths.length}]">
|
|
${params}
|
|
</ParameterList>
|
|
</cwmp:GetParameterValuesResponse>`)
|
|
}
|
|
|
|
function generateSetParameterValuesXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:SetParameterValues>
|
|
<ParameterList soap-enc:arrayType="cwmp:ParameterValueStruct[2]">
|
|
<ParameterValueStruct>
|
|
<Name>Device.ManagementServer.PeriodicInformInterval</Name>
|
|
<Value xsi:type="xsd:unsignedInt">${randomInt(60, 3600)}</Value>
|
|
</ParameterValueStruct>
|
|
<ParameterValueStruct>
|
|
<Name>Device.DeviceInfo.ProvisioningCode</Name>
|
|
<Value xsi:type="xsd:string">PROV-${randomInt(1000, 9999)}</Value>
|
|
</ParameterValueStruct>
|
|
</ParameterList>
|
|
<ParameterKey>key-${randomInt(1, 999)}</ParameterKey>
|
|
</cwmp:SetParameterValues>`)
|
|
}
|
|
|
|
function generateSetParameterValuesResponseXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:SetParameterValuesResponse>
|
|
<Status>0</Status>
|
|
</cwmp:SetParameterValuesResponse>`)
|
|
}
|
|
|
|
function generateGetParameterNamesXml(): string {
|
|
const path = randomChoice([
|
|
'Device.',
|
|
'Device.DeviceInfo.',
|
|
'Device.ManagementServer.',
|
|
'Device.WiFi.',
|
|
])
|
|
return wrapSoapEnvelope(`<cwmp:GetParameterNames>
|
|
<ParameterPath>${path}</ParameterPath>
|
|
<NextLevel>1</NextLevel>
|
|
</cwmp:GetParameterNames>`)
|
|
}
|
|
|
|
function generateGetParameterNamesResponseXml(): string {
|
|
const params = ['Device.DeviceInfo.', 'Device.ManagementServer.', 'Device.WiFi.', 'Device.IP.']
|
|
const paramList = params
|
|
.map(
|
|
(p) => `<ParameterInfoStruct>
|
|
<Name>${p}</Name>
|
|
<Writable>0</Writable>
|
|
</ParameterInfoStruct>`
|
|
)
|
|
.join('\n ')
|
|
return wrapSoapEnvelope(`<cwmp:GetParameterNamesResponse>
|
|
<ParameterList soap-enc:arrayType="cwmp:ParameterInfoStruct[${params.length}]">
|
|
${paramList}
|
|
</ParameterList>
|
|
</cwmp:GetParameterNamesResponse>`)
|
|
}
|
|
|
|
function generateDownloadXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:Download>
|
|
<CommandKey>firmware-update-${randomInt(1, 99)}</CommandKey>
|
|
<FileType>1 Firmware Upgrade Image</FileType>
|
|
<URL>http://firmware.example.com/fw-v${randomInt(1, 5)}.${randomInt(0, 9)}.bin</URL>
|
|
<Username>admin</Username>
|
|
<Password>****</Password>
|
|
<FileSize>${randomInt(1000000, 50000000)}</FileSize>
|
|
<TargetFileName></TargetFileName>
|
|
<DelaySeconds>0</DelaySeconds>
|
|
<SuccessURL></SuccessURL>
|
|
<FailureURL></FailureURL>
|
|
</cwmp:Download>`)
|
|
}
|
|
|
|
function generateDownloadResponseXml(): string {
|
|
const status = randomChoice([0, 1]) // 0=completed, 1=in progress
|
|
return wrapSoapEnvelope(`<cwmp:DownloadResponse>
|
|
<Status>${status}</Status>
|
|
<StartTime>${new Date().toISOString()}</StartTime>
|
|
<CompleteTime>0001-01-01T00:00:00Z</CompleteTime>
|
|
</cwmp:DownloadResponse>`)
|
|
}
|
|
|
|
function generateRebootXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:Reboot>
|
|
<CommandKey>reboot-${randomInt(1, 999)}</CommandKey>
|
|
</cwmp:Reboot>`)
|
|
}
|
|
|
|
function generateRebootResponseXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:RebootResponse/>`)
|
|
}
|
|
|
|
function generateFaultXml(): string {
|
|
const faultCode = randomChoice(['9001', '9002', '9003', '9005', '9007'])
|
|
const faultMessages: Record<string, string> = {
|
|
'9001': 'Request denied',
|
|
'9002': 'Internal error',
|
|
'9003': 'Invalid arguments',
|
|
'9005': 'Invalid parameter name',
|
|
'9007': 'Invalid parameter value',
|
|
}
|
|
return wrapSoapEnvelope(`<soap-env:Fault>
|
|
<faultcode>Client</faultcode>
|
|
<faultstring>CWMP fault</faultstring>
|
|
<detail>
|
|
<cwmp:Fault>
|
|
<FaultCode>${faultCode}</FaultCode>
|
|
<FaultString>${faultMessages[faultCode]}</FaultString>
|
|
</cwmp:Fault>
|
|
</detail>
|
|
</soap-env:Fault>`)
|
|
}
|
|
|
|
function generateTransferCompleteXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:TransferComplete>
|
|
<CommandKey>firmware-update-${randomInt(1, 99)}</CommandKey>
|
|
<FaultStruct>
|
|
<FaultCode>0</FaultCode>
|
|
<FaultString></FaultString>
|
|
</FaultStruct>
|
|
<StartTime>${new Date().toISOString()}</StartTime>
|
|
<CompleteTime>${new Date().toISOString()}</CompleteTime>
|
|
</cwmp:TransferComplete>`)
|
|
}
|
|
|
|
function generateTransferCompleteResponseXml(): string {
|
|
return wrapSoapEnvelope(`<cwmp:TransferCompleteResponse/>`)
|
|
}
|
|
|
|
// ─── Exchange Sequence Patterns ─────────────────────────────────────────────
|
|
|
|
/** Realistic CWMP session exchange patterns (request/response pairs) */
|
|
const SESSION_PATTERNS: ExchangeTemplate[][] = [
|
|
// Inform session start
|
|
[
|
|
{ cwmpMethod: 'Inform', direction: 'CPE→ACS', xmlGenerator: () => '' },
|
|
{ cwmpMethod: 'InformResponse', direction: 'ACS→CPE', xmlGenerator: generateInformResponseXml },
|
|
],
|
|
// GetParameterValues
|
|
[
|
|
{
|
|
cwmpMethod: 'GetParameterValues',
|
|
direction: 'ACS→CPE',
|
|
xmlGenerator: () =>
|
|
generateGetParameterValuesXml([
|
|
'Device.DeviceInfo.SoftwareVersion',
|
|
'Device.DeviceInfo.HardwareVersion',
|
|
'Device.ManagementServer.PeriodicInformInterval',
|
|
]),
|
|
},
|
|
{
|
|
cwmpMethod: 'GetParameterValuesResponse',
|
|
direction: 'CPE→ACS',
|
|
xmlGenerator: () =>
|
|
generateGetParameterValuesResponseXml([
|
|
'Device.DeviceInfo.SoftwareVersion',
|
|
'Device.DeviceInfo.HardwareVersion',
|
|
'Device.ManagementServer.PeriodicInformInterval',
|
|
]),
|
|
},
|
|
],
|
|
|
|
// SetParameterValues
|
|
[
|
|
{
|
|
cwmpMethod: 'SetParameterValues',
|
|
direction: 'ACS→CPE',
|
|
xmlGenerator: generateSetParameterValuesXml,
|
|
},
|
|
{
|
|
cwmpMethod: 'SetParameterValuesResponse',
|
|
direction: 'CPE→ACS',
|
|
xmlGenerator: generateSetParameterValuesResponseXml,
|
|
},
|
|
],
|
|
// GetParameterNames
|
|
[
|
|
{
|
|
cwmpMethod: 'GetParameterNames',
|
|
direction: 'ACS→CPE',
|
|
xmlGenerator: generateGetParameterNamesXml,
|
|
},
|
|
{
|
|
cwmpMethod: 'GetParameterNamesResponse',
|
|
direction: 'CPE→ACS',
|
|
xmlGenerator: generateGetParameterNamesResponseXml,
|
|
},
|
|
],
|
|
// Download (firmware)
|
|
[
|
|
{ cwmpMethod: 'Download', direction: 'ACS→CPE', xmlGenerator: generateDownloadXml },
|
|
{
|
|
cwmpMethod: 'DownloadResponse',
|
|
direction: 'CPE→ACS',
|
|
xmlGenerator: generateDownloadResponseXml,
|
|
},
|
|
],
|
|
// Reboot
|
|
[
|
|
{ cwmpMethod: 'Reboot', direction: 'ACS→CPE', xmlGenerator: generateRebootXml },
|
|
{ cwmpMethod: 'RebootResponse', direction: 'CPE→ACS', xmlGenerator: generateRebootResponseXml },
|
|
],
|
|
// TransferComplete (CPE-initiated after download)
|
|
[
|
|
{
|
|
cwmpMethod: 'TransferComplete',
|
|
direction: 'CPE→ACS',
|
|
xmlGenerator: generateTransferCompleteXml,
|
|
},
|
|
{
|
|
cwmpMethod: 'TransferCompleteResponse',
|
|
direction: 'ACS→CPE',
|
|
xmlGenerator: generateTransferCompleteResponseXml,
|
|
},
|
|
],
|
|
// Fault response
|
|
[{ cwmpMethod: 'Fault', direction: 'CPE→ACS', xmlGenerator: generateFaultXml }],
|
|
// Empty POST (session end)
|
|
[{ cwmpMethod: 'Empty', direction: 'CPE→ACS', xmlGenerator: () => '' }],
|
|
]
|
|
|
|
// ─── Exchange Generator ─────────────────────────────────────────────────────
|
|
|
|
interface CwmpExchangeDoc {
|
|
deviceId: string
|
|
direction: Direction
|
|
timestamp: Date
|
|
sessionId: string
|
|
cwmpMethod: string
|
|
rawXml: string
|
|
sequenceNumber: number
|
|
createdAt: Date
|
|
}
|
|
|
|
function generateExchangesForDevice(
|
|
deviceId: string,
|
|
deviceIndex: number,
|
|
count: number
|
|
): CwmpExchangeDoc[] {
|
|
const exchanges: CwmpExchangeDoc[] = []
|
|
const now = Date.now()
|
|
const twentyFourHoursMs = 24 * 60 * 60 * 1000
|
|
|
|
// Distribute exchanges over the last 24h
|
|
let sequenceNumber = 1
|
|
let currentTimeMs = now - twentyFourHoursMs + randomInt(0, 60000)
|
|
|
|
while (exchanges.length < count) {
|
|
// Start a new session
|
|
const sessionId = generateSessionId()
|
|
// Each session has an Inform + some exchanges
|
|
const sessionSize = Math.min(randomInt(4, 12), count - exchanges.length)
|
|
|
|
// Always start session with Inform
|
|
const informXml = generateInformXml(deviceIndex)
|
|
const informTimestamp = new Date(currentTimeMs)
|
|
exchanges.push({
|
|
deviceId,
|
|
direction: 'CPE→ACS',
|
|
timestamp: informTimestamp,
|
|
sessionId,
|
|
cwmpMethod: 'Inform',
|
|
rawXml: informXml,
|
|
sequenceNumber: sequenceNumber++,
|
|
createdAt: informTimestamp,
|
|
})
|
|
currentTimeMs += randomInt(50, 200) // 50-200ms between messages
|
|
|
|
if (exchanges.length >= count) break
|
|
|
|
// InformResponse
|
|
const informRespTimestamp = new Date(currentTimeMs)
|
|
exchanges.push({
|
|
deviceId,
|
|
direction: 'ACS→CPE',
|
|
timestamp: informRespTimestamp,
|
|
sessionId,
|
|
cwmpMethod: 'InformResponse',
|
|
rawXml: generateInformResponseXml(),
|
|
sequenceNumber: sequenceNumber++,
|
|
createdAt: informRespTimestamp,
|
|
})
|
|
currentTimeMs += randomInt(50, 200)
|
|
|
|
// Fill rest of session with random patterns
|
|
let remaining = sessionSize - 2
|
|
while (remaining > 0 && exchanges.length < count) {
|
|
// Pick a random pattern (skip Inform pattern at index 0)
|
|
const patternIndex = randomInt(1, SESSION_PATTERNS.length - 1)
|
|
const pattern = SESSION_PATTERNS[patternIndex]
|
|
|
|
for (const template of pattern) {
|
|
if (exchanges.length >= count) break
|
|
remaining--
|
|
|
|
const ts = new Date(currentTimeMs)
|
|
exchanges.push({
|
|
deviceId,
|
|
direction: template.direction,
|
|
timestamp: ts,
|
|
sessionId,
|
|
cwmpMethod: template.cwmpMethod,
|
|
rawXml: template.xmlGenerator(),
|
|
sequenceNumber: sequenceNumber++,
|
|
createdAt: ts,
|
|
})
|
|
currentTimeMs += randomInt(50, 300)
|
|
}
|
|
}
|
|
|
|
// Gap between sessions (30s to 30min)
|
|
currentTimeMs += randomInt(30_000, 1_800_000)
|
|
}
|
|
|
|
return exchanges.slice(0, count)
|
|
}
|
|
|
|
// ─── Seeder Runner ──────────────────────────────────────────────────────────
|
|
|
|
export async function seed(db: Db, config?: Partial<SeederConfig>): Promise<void> {
|
|
const cfg = { ...getConfig(), ...config }
|
|
const collection = db.collection('cwmp_exchanges')
|
|
const countersCollection = db.collection('cwmp_exchange_counters')
|
|
|
|
console.log(`[Seeder] Seeding cwmp_exchanges with:`)
|
|
console.log(` - ${cfg.deviceCount} devices`)
|
|
console.log(` - ${cfg.exchangesMin}-${cfg.exchangesMax} exchanges per device`)
|
|
|
|
// Clear existing data
|
|
await collection.deleteMany({})
|
|
await countersCollection.deleteMany({})
|
|
console.log(`[Seeder] Cleared existing cwmp_exchanges and counters`)
|
|
|
|
let totalInserted = 0
|
|
|
|
for (let i = 0; i < cfg.deviceCount; i++) {
|
|
const deviceId = generateDeviceId(i)
|
|
const exchangeCount = randomInt(cfg.exchangesMin, cfg.exchangesMax)
|
|
const exchanges = generateExchangesForDevice(deviceId, i, exchangeCount)
|
|
|
|
// Bulk insert exchanges
|
|
if (exchanges.length > 0) {
|
|
await collection.insertMany(exchanges)
|
|
|
|
// Update counter for the device
|
|
const maxSeq = exchanges[exchanges.length - 1].sequenceNumber
|
|
await countersCollection.updateOne(
|
|
{ _id: deviceId as any },
|
|
{ $set: { seq: maxSeq } },
|
|
{ upsert: true }
|
|
)
|
|
|
|
totalInserted += exchanges.length
|
|
console.log(`[Seeder] Device "${deviceId}": ${exchanges.length} exchanges (seq 1-${maxSeq})`)
|
|
}
|
|
}
|
|
|
|
console.log(`[Seeder] Done. Total exchanges inserted: ${totalInserted}`)
|
|
}
|
|
|
|
// ─── Standalone Execution ───────────────────────────────────────────────────
|
|
|
|
async function main(): Promise<void> {
|
|
const config = getConfig()
|
|
|
|
console.log(`[Seeder] Connecting to MongoDB at ${config.mongoUri}/${config.dbName}...`)
|
|
const client = new MongoClient(config.mongoUri)
|
|
await client.connect()
|
|
const db = client.db(config.dbName)
|
|
|
|
try {
|
|
await seed(db, config)
|
|
} finally {
|
|
await client.close()
|
|
console.log(`[Seeder] MongoDB connection closed.`)
|
|
}
|
|
}
|
|
|
|
// Run directly if executed as a script
|
|
const isMainModule = process.argv[1]?.includes('cwmp_exchanges_seeder')
|
|
if (isMainModule) {
|
|
main().catch((err) => {
|
|
console.error('[Seeder] Fatal error:', err)
|
|
process.exit(1)
|
|
})
|
|
}
|