feat: TR-069 Exchange Viewer - structured MongoDB storage with SSE streaming and sequence diagram UI
- 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
This commit is contained in:
1 parent
3fefbf6c40
commit
0c83a4ccb2
50 files changed
+15731
-43
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { MongoMigration } from '../mongo_migrator.js'
|
||||
|
||||
export default class CreateCwmpExchangesCollection extends MongoMigration {
|
||||
async up(): Promise<void> {
|
||||
const exchanges = this.db.collection('cwmp_exchanges')
|
||||
|
||||
// Compound index for efficient retrieval by device, ordered by time
|
||||
await exchanges.createIndex(
|
||||
{ deviceId: 1, timestamp: 1 },
|
||||
{ name: 'idx_deviceId_timestamp' }
|
||||
)
|
||||
|
||||
// Unique index for atomic sequence numbering and Last-Event-ID queries
|
||||
await exchanges.createIndex(
|
||||
{ deviceId: 1, sequenceNumber: 1 },
|
||||
{ unique: true, name: 'idx_deviceId_sequenceNumber' }
|
||||
)
|
||||
|
||||
// TTL index to auto-remove exchanges older than 24h
|
||||
await exchanges.createIndex(
|
||||
{ createdAt: 1 },
|
||||
{ expireAfterSeconds: 86400, name: 'idx_createdAt_ttl' }
|
||||
)
|
||||
|
||||
// Create counters collection for atomic sequence number generation
|
||||
this.db.collection('cwmp_exchange_counters')
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
await this.db.dropCollection('cwmp_exchanges').catch(() => {})
|
||||
await this.db.dropCollection('cwmp_exchange_counters').catch(() => {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user