diff --git a/forge/db/controllers/Device.js b/forge/db/controllers/Device.js index e29c80035c..38b490e2b0 100644 --- a/forge/db/controllers/Device.js +++ b/forge/db/controllers/Device.js @@ -47,6 +47,9 @@ module.exports = { if (state.nodeRedVersion) { device.set('nodeRedVersion', state.nodeRedVersion) } + if (state.nodejsVersion) { + device.set('nodejsVersion', state.nodejsVersion) + } device.set('editorAffinity', state.affinity || null) if (!state.snapshot || state.snapshot === '0') { if (device.activeSnapshotId !== null) { diff --git a/forge/db/migrations/20260622-01-add-device-nodejs-ver.js b/forge/db/migrations/20260622-01-add-device-nodejs-ver.js new file mode 100644 index 0000000000..bc5d5ba46f --- /dev/null +++ b/forge/db/migrations/20260622-01-add-device-nodejs-ver.js @@ -0,0 +1,15 @@ +const { DataTypes } = require('sequelize') + +module.exports = { + /** + * upgrade database + * @param {QueryInterface} context Sequelize.QueryInterface + */ + up: async (context, Sequelize) => { + await context.addColumn('Devices', 'nodejsVersion', { + type: DataTypes.STRING, + allowNull: true + }) + }, + down: async (context, Squelize) => { } +} diff --git a/forge/db/models/Device.js b/forge/db/models/Device.js index 7974026a8e..a5345fa347 100644 --- a/forge/db/models/Device.js +++ b/forge/db/models/Device.js @@ -53,7 +53,8 @@ module.exports = { get () { return this.ownerType === 'application' } - } + }, + nodejsVersion: { type: DataTypes.STRING, allowNull: true } }, associations: function (M) { this.belongsTo(M.Application) @@ -403,8 +404,13 @@ module.exports = { let nodeRedVersion = '3.0.2' // default to older Node-RED if (SemVer.satisfies(SemVer.coerce(this.agentVersion), '>=1.11.2')) { // 1.11.2 includes fix for ESM loading of GOT, so lets use 'latest' as before - // pinning to NR 4.1.x while we fix the device agent - nodeRedVersion = '~4.1.11' + if (this.nodejsVersion) { + if (SemVer.satisfies(SemVer.coerce(this.nodejsVersion), '>=22.9.0')) { + nodeRedVersion = 'latest' + } + } else { + nodeRedVersion = '~4.1.11' + } } return nodeRedVersion }, diff --git a/test/unit/forge/routes/api/device_spec.js b/test/unit/forge/routes/api/device_spec.js index 36ea0a3d4e..7bded4b02e 100644 --- a/test/unit/forge/routes/api/device_spec.js +++ b/test/unit/forge/routes/api/device_spec.js @@ -767,7 +767,7 @@ describe('Device API', async function () { result.should.have.property('modules').and.be.an.Object() result.modules.should.have.property('node-red', '3.0.2') }) - it('agent >= v1.11.2 is instructed to use Node-RED@latest', async function () { + it('agent >= v1.11.2 is instructed to use Node-RED@4.1.11 if NodeJS version unknown', async function () { const agentVersion = '1.11.2' // min agent version required for NR 3.1 (as this agent handles ESM issue) const device = await createDevice({ name: 'Ad1a', type: '', team: TestObjects.ATeam.hashid, as: TestObjects.tokens.alice, agentVersion }) // assign the new device to application @@ -793,6 +793,35 @@ describe('Device API', async function () { result.should.have.property('modules').and.be.an.Object() result.modules.should.have.property('node-red', '~4.1.11') }) + it('agent >= v1.11.2 is instructed to use Node-RED@latest if NodeJS >=22.9.0', async function () { + const agentVersion = '1.11.2' // min agent version required for NR 3.1 (as this agent handles ESM issue) + const device = await createDevice({ name: 'Ad1b', type: '', team: TestObjects.ATeam.hashid, as: TestObjects.tokens.alice, agentVersion }) + const dbDevice = await app.db.models.Device.byId(device.id) + dbDevice.nodejsVersion = 'v24.0.0' + await dbDevice.save() + // assign the new device to application + await app.inject({ + method: 'PUT', + url: `/api/v1/devices/${device.id}`, + body: { + application: TestObjects.Application1.hashid + }, + cookies: { sid: TestObjects.tokens.bob } + }) + // get the snapshot for this device + const response = await app.inject({ + method: 'GET', + url: `/api/v1/devices/${device.id}/live/snapshot`, + headers: { + authorization: `Bearer ${device.credentials.token}` + } + }) + const result = response.json() + result.should.have.property('id') + result.should.have.property('name', 'Starter Snapshot') + result.should.have.property('modules').and.be.an.Object() + result.modules.should.have.property('node-red', 'latest') + }) it('snapshot uploaded without node-red dependency is always delivered to a device with the node-red:version', async function () { const agentVersion = '1.11.2' // min agent version required for NR 3.1 (as this agent handles ESM issue) const device = await createDevice({ name: 'Ad1a', type: '', team: TestObjects.ATeam.hashid, as: TestObjects.tokens.alice, agentVersion })