Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/initMPS.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ CREATE TABLE IF NOT EXISTS devices(
lastseen timestamp with time zone,
lastdisconnected timestamp with time zone,
deviceinfo JSON,
powerstate integer,
ospowersavingstate integer,
powerstateupdatedat timestamp with time zone,
Comment on lines +23 to +25
CONSTRAINT device_guid UNIQUE(guid),
PRIMARY KEY (guid,tenantid)
);
42 changes: 42 additions & 0 deletions src/data/mongo/collections/device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('MongoDeviceTable', () => {
insertOne: vi.fn(),
findOneAndUpdate: vi.fn(),
distinct: vi.fn(),
updateOne: vi.fn(),
updateMany: vi.fn()
} as any

Expand Down Expand Up @@ -156,6 +157,47 @@ describe('MongoDeviceTable', () => {
expect(result).toEqual(mockData)
})

it('should update the power state for a device', async () => {
const updatedAt = new Date('2026-08-25T17:00:00.000Z')
collection.updateOne.mockResolvedValue({ matchedCount: 1 } as any)

const result = await mongoDeviceTable.updatePowerState('someGuid', 4, 2, updatedAt, 'someTenantId')

expect(result).toBe(true)
expect(collection.updateOne).toHaveBeenCalledWith(
{ guid: 'someGuid', tenantId: 'someTenantId' },
{ $set: { powerState: 4, osPowerSavingState: 2, powerStateUpdatedAt: updatedAt } }
)
})

it('should default to an empty tenantId when the power state update omits it', async () => {
const updatedAt = new Date('2026-08-25T17:00:00.000Z')
collection.updateOne.mockResolvedValue({ matchedCount: 1 } as any)

await mongoDeviceTable.updatePowerState('someGuid', 4, 2, updatedAt)

expect(collection.updateOne).toHaveBeenCalledWith(
{ guid: 'someGuid', tenantId: '' },
{ $set: { powerState: 4, osPowerSavingState: 2, powerStateUpdatedAt: updatedAt } }
)
})

it('should return false when no device matches the power state update', async () => {
collection.updateOne.mockResolvedValue({ matchedCount: 0 } as any)

const result = await mongoDeviceTable.updatePowerState('someGuid', 4, 2, new Date(), 'someTenantId')

expect(result).toBe(false)
})

it('should reject when the power state update fails so the caller can back off', async () => {
collection.updateOne.mockRejectedValue(new Error('mongo is down'))

await expect(mongoDeviceTable.updatePowerState('someGuid', 4, 2, new Date(), 'someTenantId')).rejects.toThrow(
'mongo is down'
)
})

it('should clear instance status', async () => {
collection.updateMany.mockResolvedValue({ modifiedCount: 5 } as any)

Expand Down
15 changes: 15 additions & 0 deletions src/data/mongo/collections/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ export class MongoDeviceTable implements IDeviceTable {
return this.collection.find({ hostname, tenantId }).toArray() as unknown as WithId<Device>[]
}

async updatePowerState(
guid: string,
powerState: number,
osPowerSavingState: number,
updatedAt: Date,
tenantId = ''
): Promise<boolean> {
const result = await this.collection.updateOne(
{ guid, tenantId },
{ $set: { powerState, osPowerSavingState, powerStateUpdatedAt: updatedAt } }
)
// matchedCount, not modifiedCount: re-writing an unchanged power state is a successful no-op
return result.matchedCount > 0
}

async clearInstanceStatus(mpsInstance: string): Promise<boolean> {
const result = await this.collection.updateMany(
{ mpsInstance },
Expand Down
105 changes: 97 additions & 8 deletions src/data/postgres/tables/device.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ describe('device tests', () => {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tenantid = $3
ORDER BY guid
Expand Down Expand Up @@ -105,7 +108,10 @@ describe('device tests', () => {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tenantid = $3
ORDER BY guid
Expand Down Expand Up @@ -139,7 +145,10 @@ describe('device tests', () => {
lastconnected as "lastConnected",
lastseen as "lastSeen",
lastdisconnected as "lastDisconnected",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE guid = $1`,
['4c4c4544-004b-4210-8033-b6c04f504633']
Expand Down Expand Up @@ -202,7 +211,10 @@ describe('device tests', () => {
lastconnected as "lastConnected",
lastseen as "lastSeen",
lastdisconnected as "lastDisconnected",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE guid = $1 and tenantid = $2`,
['4c4c4544-004b-4210-8033-b6c04f504633', 'tenantId']
Expand Down Expand Up @@ -235,7 +247,10 @@ describe('device tests', () => {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE hostname = $1 and tenantid = $2`,
['hostname', 'tenantId']
Expand All @@ -259,7 +274,10 @@ describe('device tests', () => {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE hostname = $1 and tenantid = $2`,
['hostname', '']
Expand Down Expand Up @@ -317,7 +335,10 @@ describe('device tests', () => {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tags @> $1 and tenantId = $4
ORDER BY guid
Expand Down Expand Up @@ -357,7 +378,10 @@ describe('device tests', () => {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tags && $1 and tenantId = $4
ORDER BY guid
Expand Down Expand Up @@ -671,6 +695,71 @@ describe('device tests', () => {
expect(mpsError).toBeInstanceOf(MPSValidationError)
})

test('should leave the power state columns alone when updating a device', async () => {
// both writers of this row must keep disjoint SET lists or one clobbers the other
querySpy.mockResolvedValueOnce({ rows: [], command: '', fields: null, rowCount: 1, oid: 0 })
querySpy.mockResolvedValueOnce({ rows: [{}], command: '', fields: null, rowCount: 1, oid: 0 })
await deviceTable.update({ guid: '4c4c4544-004b-4210-8033-b6c04f504633', tenantId: '' } as Device)
const sql = querySpy.mock.calls[0][0]
expect(sql).toContain('UPDATE devices')
expect(sql).not.toContain('powerstate')
expect(sql).not.toContain('ospowersavingstate')
expect(sql).not.toContain('powerstateupdatedat')
})

test('should get true when power state is updated', async () => {
const updatedAt = new Date('2026-08-25T17:00:00.000Z')
querySpy.mockResolvedValueOnce({ rows: [], command: '', fields: null, rowCount: 1, oid: 0 })
const result = await deviceTable.updatePowerState(
'4c4c4544-004b-4210-8033-b6c04f504633',
4,
2,
updatedAt,
'tenantId'
)
expect(result).toBe(true)
expect(querySpy).toHaveBeenCalledTimes(1)
expect(querySpy).toHaveBeenCalledWith(
`
UPDATE devices
SET powerstate=$2, ospowersavingstate=$3, powerstateupdatedat=$4
WHERE guid=$1 and tenantid = $5`,
[
'4c4c4544-004b-4210-8033-b6c04f504633',
4,
2,
updatedAt,
'tenantId'
]
)
})

test('should default to an empty tenantId when the power state update omits it', async () => {
const updatedAt = new Date('2026-08-25T17:00:00.000Z')
querySpy.mockResolvedValueOnce({ rows: [], command: '', fields: null, rowCount: 1, oid: 0 })
await deviceTable.updatePowerState('4c4c4544-004b-4210-8033-b6c04f504633', 4, 2, updatedAt)
expect(querySpy).toHaveBeenCalledWith(expect.any(String), [
'4c4c4544-004b-4210-8033-b6c04f504633',
4,
2,
updatedAt,
''
])
})

test('should get false when no device matches the power state update', async () => {
querySpy.mockResolvedValueOnce({ rows: [], command: '', fields: null, rowCount: 0, oid: 0 })
const result = await deviceTable.updatePowerState('4c4c4544-004b-4210-8033-b6c04f504633', 4, 2, new Date())
expect(result).toBe(false)
})

test('should reject when the power state update fails so the caller can back off', async () => {
querySpy.mockRejectedValueOnce(new Error('db is down'))
await expect(
deviceTable.updatePowerState('4c4c4544-004b-4210-8033-b6c04f504633', 4, 2, new Date())
).rejects.toThrow('db is down')
})

test('should get true when device connection status update', async () => {
querySpy.mockResolvedValueOnce({ rows: [], command: '', fields: null, rowCount: 1, oid: 0 })
const result = await deviceTable.clearInstanceStatus('localhost')
Expand Down
62 changes: 56 additions & 6 deletions src/data/postgres/tables/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export class DeviceTable implements IDeviceTable {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tenantid = $3
ORDER BY guid
Expand Down Expand Up @@ -100,7 +103,10 @@ export class DeviceTable implements IDeviceTable {
lastconnected as "lastConnected",
lastseen as "lastSeen",
lastdisconnected as "lastDisconnected",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE guid = $1 and tenantid = $2`
let params = [id, tenantId]
Expand All @@ -118,7 +124,10 @@ export class DeviceTable implements IDeviceTable {
lastconnected as "lastConnected",
lastseen as "lastSeen",
lastdisconnected as "lastDisconnected",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE guid = $1`
params = [id]
Expand All @@ -140,7 +149,10 @@ export class DeviceTable implements IDeviceTable {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE ${columnName} = $1 and tenantid = $2`,
[queryValue, tenantId]
Expand Down Expand Up @@ -178,7 +190,10 @@ export class DeviceTable implements IDeviceTable {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tags @> $1 and tenantId = $4
ORDER BY guid
Expand All @@ -204,7 +219,10 @@ export class DeviceTable implements IDeviceTable {
tenantid as "tenantId",
friendlyname as "friendlyName",
dnssuffix as "dnsSuffix",
deviceinfo as "deviceInfo"
deviceinfo as "deviceInfo",
powerstate as "powerState",
ospowersavingstate as "osPowerSavingState",
powerstateupdatedat as "powerStateUpdatedAt"
FROM devices
WHERE tags && $1 and tenantId = $4
ORDER BY guid
Expand Down Expand Up @@ -307,6 +325,38 @@ export class DeviceTable implements IDeviceTable {
}
}

/**
* @description Update the cached power state for a device
* @param {string} guid
* @param {number} powerState DMTF power state value read from the device
* @param {number} osPowerSavingState OS power saving state read from the device
* @param {Date} updatedAt time the power state was read
* @param {string} [tenantId] tenant the device belongs to
* @returns {boolean} Return true when a device row matched and was updated
*/
async updatePowerState(
guid: string,
powerState: number,
osPowerSavingState: number,
updatedAt: Date,
tenantId = ''
): Promise<boolean> {
const results = await this.db.query(
`
UPDATE devices
SET powerstate=$2, ospowersavingstate=$3, powerstateupdatedat=$4
WHERE guid=$1 and tenantid = $5`,
[
guid,
powerState,
osPowerSavingState,
updatedAt,
tenantId
]
)
return results.rowCount > 0
}

/**
* @description Clear the mpsInstance for associated devices before process exit
* @param {string} mpsInstance
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/IDeviceTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,12 @@ export interface IDeviceTable extends ITable<Device> {
) => Promise<Device[]>
getByFriendlyName: (hostname: string, tenantId?: string) => Promise<Device[]>
getByHostname: (hostname: string, tenantId?: string) => Promise<Device[]>
updatePowerState: (
guid: string,
powerState: number,
osPowerSavingState: number,
updatedAt: Date,
tenantId?: string
) => Promise<boolean>
Comment on lines +21 to +27
clearInstanceStatus: (mpsInstance: string) => Promise<boolean>
}
3 changes: 3 additions & 0 deletions src/models/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface Device {
lastSeen?: Date
lastDisconnected?: Date
deviceInfo?: DeviceInfo
powerState?: number
osPowerSavingState?: number
powerStateUpdatedAt?: Date
Comment on lines +25 to +27
}
export interface DeviceInfo {
fwVersion: string
Expand Down
Loading
Loading