From 653a1eec6e1dd504fe38f1faa873a86893c1540b Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 7 Jul 2026 11:27:22 +0100 Subject: [PATCH 01/12] Build a cache of latest NR packages fixes #7735 --- forge/ee/lib/bom/index.js | 5 +++ forge/ee/lib/bom/tasks/cache-catalogues.js | 51 ++++++++++++++++++++++ forge/ee/lib/index.js | 2 + 3 files changed, 58 insertions(+) create mode 100644 forge/ee/lib/bom/index.js create mode 100644 forge/ee/lib/bom/tasks/cache-catalogues.js diff --git a/forge/ee/lib/bom/index.js b/forge/ee/lib/bom/index.js new file mode 100644 index 0000000000..4883d21215 --- /dev/null +++ b/forge/ee/lib/bom/index.js @@ -0,0 +1,5 @@ +module.exports.init = async function (app) { + app.config.features.register('autoNodeUpdate', true, true) + + app.housekeeper.registerTask(require('./tasks/cache-catalogues')) +} diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js new file mode 100644 index 0000000000..83f9dc892f --- /dev/null +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -0,0 +1,51 @@ +const { decodeCertifiedNodesToken } = require('../../../../lib/npm') +const { randomInt } = require("../../../../housekeeper/utils") + +const axios = require('axios') + +const NODE_VERSION_CACHE = 'nodes-latestVersion' + +module.exports = { + name: 'cacheCatalogues', + //startup: false, + startup: 1000 * 10, + schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + run: async function (app) { + app.caches.createCache(NODE_VERSION_CACHE) + const cache = app.caches.getCache(NODE_VERSION_CACHE) + // Starting list of catalogues + const cataloguesList = [ + 'https://catalogue.nodered.org/catalogue.json' + ] + + const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes', false) && + !!app.config.features.enabled('ffNodes', false) && + !!app.settings.get('platform:ff-npm-registry:token') + if (platformNPMEnabled) { + // gets platform wide certified nodes catalogues but not per team override + const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + cataloguesList.push(...catalogues) + } + + for (const cat of cataloguesList) { + app.log.debug(`Checking catalogue ${cat} for latest node versions`) + try { + const res = await axios.get(cat, { + headers: { + Accept: 'application/json' + } + }) + if (res.status === 200) { + const modules = res.data.modules + for (const mod of modules) { + const name = mod.id + const version = mod.version + cache.set(name, version) + } + } + } catch (err) { + app.log.debug(`Problem reading catalogue ${cat}, ${err.toString()}`) + } + } + } +} \ No newline at end of file diff --git a/forge/ee/lib/index.js b/forge/ee/lib/index.js index 65abf24ef8..86f2d2a9e3 100644 --- a/forge/ee/lib/index.js +++ b/forge/ee/lib/index.js @@ -58,6 +58,8 @@ module.exports = fp(async function (app, opts) { // temporary until FF Expert Insights can be enabled on Self Hosted EE instance const isInsightsEnabled = isAiEnabled && app.config?.expert?.enabled && app.config?.expert?.insights?.enabled app.config.features.register('expertInsights', isInsightsEnabled ?? false, false) + + require('./bom').init(app) } // Set the Team Library Feature Flag From da408e9458e892870af33ae0024cf0be6d2d3be4 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Wed, 8 Jul 2026 09:17:34 +0100 Subject: [PATCH 02/12] fix lint --- forge/ee/lib/bom/tasks/cache-catalogues.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js index 83f9dc892f..e19c3ffe25 100644 --- a/forge/ee/lib/bom/tasks/cache-catalogues.js +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -1,15 +1,13 @@ -const { decodeCertifiedNodesToken } = require('../../../../lib/npm') -const { randomInt } = require("../../../../housekeeper/utils") - const axios = require('axios') +const { decodeCertifiedNodesToken } = require('../../../../lib/npm') + const NODE_VERSION_CACHE = 'nodes-latestVersion' module.exports = { name: 'cacheCatalogues', - //startup: false, - startup: 1000 * 10, - schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + startup: 1000 * 90, // 90 seconds after start up to ensure cache populated + schedule: '47 21 * * *', // Update at 21:47 every day (if not done with a restart) run: async function (app) { app.caches.createCache(NODE_VERSION_CACHE) const cache = app.caches.getCache(NODE_VERSION_CACHE) @@ -23,11 +21,11 @@ module.exports = { !!app.settings.get('platform:ff-npm-registry:token') if (platformNPMEnabled) { // gets platform wide certified nodes catalogues but not per team override - const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + const { catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') cataloguesList.push(...catalogues) } - for (const cat of cataloguesList) { + for (const cat of cataloguesList) { app.log.debug(`Checking catalogue ${cat} for latest node versions`) try { const res = await axios.get(cat, { @@ -42,10 +40,10 @@ module.exports = { const version = mod.version cache.set(name, version) } - } + } } catch (err) { app.log.debug(`Problem reading catalogue ${cat}, ${err.toString()}`) } } } -} \ No newline at end of file +} From 4075f6dfe4f27a671fb8196631b3e3981366b982 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 7 Jul 2026 11:27:22 +0100 Subject: [PATCH 03/12] Build a cache of latest NR packages fixes #7735 --- forge/ee/lib/bom/index.js | 5 +++ forge/ee/lib/bom/tasks/cache-catalogues.js | 51 ++++++++++++++++++++++ forge/ee/lib/index.js | 2 + 3 files changed, 58 insertions(+) create mode 100644 forge/ee/lib/bom/index.js create mode 100644 forge/ee/lib/bom/tasks/cache-catalogues.js diff --git a/forge/ee/lib/bom/index.js b/forge/ee/lib/bom/index.js new file mode 100644 index 0000000000..4883d21215 --- /dev/null +++ b/forge/ee/lib/bom/index.js @@ -0,0 +1,5 @@ +module.exports.init = async function (app) { + app.config.features.register('autoNodeUpdate', true, true) + + app.housekeeper.registerTask(require('./tasks/cache-catalogues')) +} diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js new file mode 100644 index 0000000000..83f9dc892f --- /dev/null +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -0,0 +1,51 @@ +const { decodeCertifiedNodesToken } = require('../../../../lib/npm') +const { randomInt } = require("../../../../housekeeper/utils") + +const axios = require('axios') + +const NODE_VERSION_CACHE = 'nodes-latestVersion' + +module.exports = { + name: 'cacheCatalogues', + //startup: false, + startup: 1000 * 10, + schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + run: async function (app) { + app.caches.createCache(NODE_VERSION_CACHE) + const cache = app.caches.getCache(NODE_VERSION_CACHE) + // Starting list of catalogues + const cataloguesList = [ + 'https://catalogue.nodered.org/catalogue.json' + ] + + const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes', false) && + !!app.config.features.enabled('ffNodes', false) && + !!app.settings.get('platform:ff-npm-registry:token') + if (platformNPMEnabled) { + // gets platform wide certified nodes catalogues but not per team override + const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + cataloguesList.push(...catalogues) + } + + for (const cat of cataloguesList) { + app.log.debug(`Checking catalogue ${cat} for latest node versions`) + try { + const res = await axios.get(cat, { + headers: { + Accept: 'application/json' + } + }) + if (res.status === 200) { + const modules = res.data.modules + for (const mod of modules) { + const name = mod.id + const version = mod.version + cache.set(name, version) + } + } + } catch (err) { + app.log.debug(`Problem reading catalogue ${cat}, ${err.toString()}`) + } + } + } +} \ No newline at end of file diff --git a/forge/ee/lib/index.js b/forge/ee/lib/index.js index 65abf24ef8..86f2d2a9e3 100644 --- a/forge/ee/lib/index.js +++ b/forge/ee/lib/index.js @@ -58,6 +58,8 @@ module.exports = fp(async function (app, opts) { // temporary until FF Expert Insights can be enabled on Self Hosted EE instance const isInsightsEnabled = isAiEnabled && app.config?.expert?.enabled && app.config?.expert?.insights?.enabled app.config.features.register('expertInsights', isInsightsEnabled ?? false, false) + + require('./bom').init(app) } // Set the Team Library Feature Flag From 6e1b9a712e36b3bd07f7853d877f375f1baf0b8c Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Wed, 8 Jul 2026 09:17:34 +0100 Subject: [PATCH 04/12] fix lint --- forge/ee/lib/bom/tasks/cache-catalogues.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js index 83f9dc892f..e19c3ffe25 100644 --- a/forge/ee/lib/bom/tasks/cache-catalogues.js +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -1,15 +1,13 @@ -const { decodeCertifiedNodesToken } = require('../../../../lib/npm') -const { randomInt } = require("../../../../housekeeper/utils") - const axios = require('axios') +const { decodeCertifiedNodesToken } = require('../../../../lib/npm') + const NODE_VERSION_CACHE = 'nodes-latestVersion' module.exports = { name: 'cacheCatalogues', - //startup: false, - startup: 1000 * 10, - schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + startup: 1000 * 90, // 90 seconds after start up to ensure cache populated + schedule: '47 21 * * *', // Update at 21:47 every day (if not done with a restart) run: async function (app) { app.caches.createCache(NODE_VERSION_CACHE) const cache = app.caches.getCache(NODE_VERSION_CACHE) @@ -23,11 +21,11 @@ module.exports = { !!app.settings.get('platform:ff-npm-registry:token') if (platformNPMEnabled) { // gets platform wide certified nodes catalogues but not per team override - const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + const { catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') cataloguesList.push(...catalogues) } - for (const cat of cataloguesList) { + for (const cat of cataloguesList) { app.log.debug(`Checking catalogue ${cat} for latest node versions`) try { const res = await axios.get(cat, { @@ -42,10 +40,10 @@ module.exports = { const version = mod.version cache.set(name, version) } - } + } } catch (err) { app.log.debug(`Problem reading catalogue ${cat}, ${err.toString()}`) } } } -} \ No newline at end of file +} From 02679f0c21aaa3c628a4a0ad505587846c2365e6 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 7 Jul 2026 11:27:22 +0100 Subject: [PATCH 05/12] Build a cache of latest NR packages fixes #7735 --- forge/ee/lib/bom/index.js | 5 +++ forge/ee/lib/bom/tasks/cache-catalogues.js | 51 ++++++++++++++++++++++ forge/ee/lib/index.js | 2 + 3 files changed, 58 insertions(+) create mode 100644 forge/ee/lib/bom/index.js create mode 100644 forge/ee/lib/bom/tasks/cache-catalogues.js diff --git a/forge/ee/lib/bom/index.js b/forge/ee/lib/bom/index.js new file mode 100644 index 0000000000..4883d21215 --- /dev/null +++ b/forge/ee/lib/bom/index.js @@ -0,0 +1,5 @@ +module.exports.init = async function (app) { + app.config.features.register('autoNodeUpdate', true, true) + + app.housekeeper.registerTask(require('./tasks/cache-catalogues')) +} diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js new file mode 100644 index 0000000000..83f9dc892f --- /dev/null +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -0,0 +1,51 @@ +const { decodeCertifiedNodesToken } = require('../../../../lib/npm') +const { randomInt } = require("../../../../housekeeper/utils") + +const axios = require('axios') + +const NODE_VERSION_CACHE = 'nodes-latestVersion' + +module.exports = { + name: 'cacheCatalogues', + //startup: false, + startup: 1000 * 10, + schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + run: async function (app) { + app.caches.createCache(NODE_VERSION_CACHE) + const cache = app.caches.getCache(NODE_VERSION_CACHE) + // Starting list of catalogues + const cataloguesList = [ + 'https://catalogue.nodered.org/catalogue.json' + ] + + const platformNPMEnabled = !!app.config.features.enabled('certifiedNodes', false) && + !!app.config.features.enabled('ffNodes', false) && + !!app.settings.get('platform:ff-npm-registry:token') + if (platformNPMEnabled) { + // gets platform wide certified nodes catalogues but not per team override + const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + cataloguesList.push(...catalogues) + } + + for (const cat of cataloguesList) { + app.log.debug(`Checking catalogue ${cat} for latest node versions`) + try { + const res = await axios.get(cat, { + headers: { + Accept: 'application/json' + } + }) + if (res.status === 200) { + const modules = res.data.modules + for (const mod of modules) { + const name = mod.id + const version = mod.version + cache.set(name, version) + } + } + } catch (err) { + app.log.debug(`Problem reading catalogue ${cat}, ${err.toString()}`) + } + } + } +} \ No newline at end of file diff --git a/forge/ee/lib/index.js b/forge/ee/lib/index.js index dff8995d87..e44e9179b1 100644 --- a/forge/ee/lib/index.js +++ b/forge/ee/lib/index.js @@ -61,6 +61,8 @@ module.exports = fp(async function (app, opts) { (Object.prototype.hasOwnProperty.call(app.config?.expert ?? {}, 'insights') ? !!app.config?.expert?.insights?.enabled : true) app.config.features.register('expertInsights', isInsightsEnabled ?? false, true) + + require('./bom').init(app) } // Set the Team Library Feature Flag From 5cd8d1f4a409a0c50d31565f254c6ad930f042c3 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Wed, 8 Jul 2026 09:17:34 +0100 Subject: [PATCH 06/12] fix lint --- forge/ee/lib/bom/tasks/cache-catalogues.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js index 83f9dc892f..e19c3ffe25 100644 --- a/forge/ee/lib/bom/tasks/cache-catalogues.js +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -1,15 +1,13 @@ -const { decodeCertifiedNodesToken } = require('../../../../lib/npm') -const { randomInt } = require("../../../../housekeeper/utils") - const axios = require('axios') +const { decodeCertifiedNodesToken } = require('../../../../lib/npm') + const NODE_VERSION_CACHE = 'nodes-latestVersion' module.exports = { name: 'cacheCatalogues', - //startup: false, - startup: 1000 * 10, - schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + startup: 1000 * 90, // 90 seconds after start up to ensure cache populated + schedule: '47 21 * * *', // Update at 21:47 every day (if not done with a restart) run: async function (app) { app.caches.createCache(NODE_VERSION_CACHE) const cache = app.caches.getCache(NODE_VERSION_CACHE) @@ -23,11 +21,11 @@ module.exports = { !!app.settings.get('platform:ff-npm-registry:token') if (platformNPMEnabled) { // gets platform wide certified nodes catalogues but not per team override - const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + const { catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') cataloguesList.push(...catalogues) } - for (const cat of cataloguesList) { + for (const cat of cataloguesList) { app.log.debug(`Checking catalogue ${cat} for latest node versions`) try { const res = await axios.get(cat, { @@ -42,10 +40,10 @@ module.exports = { const version = mod.version cache.set(name, version) } - } + } } catch (err) { app.log.debug(`Problem reading catalogue ${cat}, ${err.toString()}`) } } } -} \ No newline at end of file +} From 42c2e640c844cfe06d8a726f76892f33578a136d Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 7 Jul 2026 11:27:22 +0100 Subject: [PATCH 07/12] Build a cache of latest NR packages fixes #7735 --- forge/ee/lib/bom/tasks/cache-catalogues.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js index e19c3ffe25..42c167a75f 100644 --- a/forge/ee/lib/bom/tasks/cache-catalogues.js +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -6,8 +6,9 @@ const NODE_VERSION_CACHE = 'nodes-latestVersion' module.exports = { name: 'cacheCatalogues', - startup: 1000 * 90, // 90 seconds after start up to ensure cache populated - schedule: '47 21 * * *', // Update at 21:47 every day (if not done with a restart) + //startup: false, + startup: 1000 * 10, + schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day run: async function (app) { app.caches.createCache(NODE_VERSION_CACHE) const cache = app.caches.getCache(NODE_VERSION_CACHE) @@ -21,11 +22,11 @@ module.exports = { !!app.settings.get('platform:ff-npm-registry:token') if (platformNPMEnabled) { // gets platform wide certified nodes catalogues but not per team override - const { catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') cataloguesList.push(...catalogues) } - for (const cat of cataloguesList) { + for (const cat of cataloguesList) { app.log.debug(`Checking catalogue ${cat} for latest node versions`) try { const res = await axios.get(cat, { From 362eb3a8af822eeca7245d7a20ead9385bcb70eb Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Wed, 8 Jul 2026 09:17:34 +0100 Subject: [PATCH 08/12] fix lint --- forge/ee/lib/bom/tasks/cache-catalogues.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js index 42c167a75f..e19c3ffe25 100644 --- a/forge/ee/lib/bom/tasks/cache-catalogues.js +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -6,9 +6,8 @@ const NODE_VERSION_CACHE = 'nodes-latestVersion' module.exports = { name: 'cacheCatalogues', - //startup: false, - startup: 1000 * 10, - schedule: `*/5 * * * *`, // '35 21 * * *' // 21:35 every day + startup: 1000 * 90, // 90 seconds after start up to ensure cache populated + schedule: '47 21 * * *', // Update at 21:47 every day (if not done with a restart) run: async function (app) { app.caches.createCache(NODE_VERSION_CACHE) const cache = app.caches.getCache(NODE_VERSION_CACHE) @@ -22,11 +21,11 @@ module.exports = { !!app.settings.get('platform:ff-npm-registry:token') if (platformNPMEnabled) { // gets platform wide certified nodes catalogues but not per team override - const { token, catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') + const { catalogues } = decodeCertifiedNodesToken(app.settings.get('platform:ff-npm-registry:token'), 'placeholder') cataloguesList.push(...catalogues) } - for (const cat of cataloguesList) { + for (const cat of cataloguesList) { app.log.debug(`Checking catalogue ${cat} for latest node versions`) try { const res = await axios.get(cat, { From 28fd7fba04297dd946a24d588537f4957dfece42 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Mon, 27 Jul 2026 17:12:20 +0100 Subject: [PATCH 09/12] Installed node settings hook (#7880) --- .../20260717-01-add-node-red-node-version.js | 46 +++++++++++++ forge/db/models/NodeREDNodeVersions.js | 66 +++++++++++++++++++ forge/db/models/Project.js | 7 ++ forge/db/models/StorageSettings.js | 30 +++++++++ forge/db/models/index.js | 3 +- forge/ee/lib/bom/tasks/cache-catalogues.js | 4 ++ forge/ee/lib/index.js | 2 +- 7 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 forge/db/migrations/20260717-01-add-node-red-node-version.js create mode 100644 forge/db/models/NodeREDNodeVersions.js diff --git a/forge/db/migrations/20260717-01-add-node-red-node-version.js b/forge/db/migrations/20260717-01-add-node-red-node-version.js new file mode 100644 index 0000000000..41f3aca376 --- /dev/null +++ b/forge/db/migrations/20260717-01-add-node-red-node-version.js @@ -0,0 +1,46 @@ +const { DataTypes } = require('sequelize') + +module.exports = { + /** + * upgrade database + * @param {QueryInterface} context Sequelize.QueryInterface + */ + up: async (context, Sequelize) => { + await context.createTable('NodeREDNodeVersions', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + ownerId: { + type: DataTypes.STRING, + allowNull: false + }, + ownerType: { + type: DataTypes.STRING, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + currentVersion: { + type: DataTypes.STRING, + allowNull: false + }, + latestVersion: { + type: DataTypes.STRING, + allowNull: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }) + }, + down: async (context, Sequelize) => {} +} diff --git a/forge/db/models/NodeREDNodeVersions.js b/forge/db/models/NodeREDNodeVersions.js new file mode 100644 index 0000000000..51601faa69 --- /dev/null +++ b/forge/db/models/NodeREDNodeVersions.js @@ -0,0 +1,66 @@ +/** + * Which nodes are installed in which Instances + * @namespace forge.db.models.NodeREDNodeVersion + * // type helpers for design time help and error checking + * @typedef {import('sequelize').Model} Model + * @typedef {import('sequelize').ModelAttributes} ModelAttributes + * @typedef {import('sequelize').SchemaOptions} SchemaOptions + * @typedef {import('sequelize').ModelIndexesOptions} ModelIndexesOptions + * @typedef {import('sequelize').InitOptions} InitOptions + * @typedef {import('sequelize').ModelScopeOptions} ModelScopeOptions + * @typedef {{name: string, schema: ModelAttributes, model: Model, indexes?: ModelIndexesOptions[], scopes?: ModelScopeOptions, options?: InitOptions}} FFModel + */ + +const { DataTypes } = require('sequelize') + +/** @type {FFModel} */ +module.exports = { + name: 'NodeREDNodeVersions', + schema: { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + ownerId: { + type: DataTypes.STRING, + allowNull: false + }, + ownerType: { + type: DataTypes.STRING, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + currentVersion: { + type: DataTypes.STRING, + allowNull: false + }, + latestVersion: { + type: DataTypes.STRING, + allowNull: true + } + }, + associations: function (M) { + this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false }) + this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false }) + }, + finders: function (M) { + return { + static: { + updateAllLatest: async (name, version) => { + await this.update({ + latestVersion: version + }, { + where: { + name + } + }) + } + }, + instance: {} + } + } +} diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index 3ca0e70653..246bcb97f5 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -260,6 +260,13 @@ module.exports = { app.log.error(`Error removing MCPRegistrations for deleted instance ${project.id}: ${err.message}`) } } + // Remove version info when Project removed + await app.db.models.NodeREDNodeVersions.destory({ + where: { + ownerType: 'instance', + ownerId: project.id + } + }) } } }, diff --git a/forge/db/models/StorageSettings.js b/forge/db/models/StorageSettings.js index dac4852517..10a62bb420 100644 --- a/forge/db/models/StorageSettings.js +++ b/forge/db/models/StorageSettings.js @@ -4,6 +4,8 @@ */ const { DataTypes } = require('sequelize') +const NODE_VERSION_CACHE = 'nodes-latestVersion' + module.exports = { name: 'StorageSettings', schema: { @@ -23,5 +25,33 @@ module.exports = { } } } + }, + hooks: function (M, app) { + return { + afterUpdate: async (storageSettings, options) => { + try { + const cache = app.caches.getCache(NODE_VERSION_CACHE) + await storageSettings.reload({ + attributes: ['id', 'settings', 'ProjectId'] + }) + const settingsObj = JSON.parse(storageSettings.settings) + const nodes = settingsObj.nodes + for (const n of Object.keys(nodes)) { + if (n !== 'node-red') { + const latest = await cache.get(n) + await app.db.models.NodeREDNodeVersions.upsert({ + ownerId: storageSettings.ProjectId, + ownerType: 'instance', + name: n, + currentVersion: nodes[n].version, + latestVersion: latest + }) + } + } + } catch (err) { + // swallowing error as this is an async hook triggered by an update to the underlying table + } + } + } } } diff --git a/forge/db/models/index.js b/forge/db/models/index.js index acb7f596ce..7b3fad9276 100644 --- a/forge/db/models/index.js +++ b/forge/db/models/index.js @@ -84,7 +84,8 @@ const modelTypes = [ 'TeamBrokerClient', 'BrokerCredentials', 'MQTTTopicSchema', - 'TeamBrokerAgent' + 'TeamBrokerAgent', + 'NodeREDNodeVersions' ] // A local map of the known models. diff --git a/forge/ee/lib/bom/tasks/cache-catalogues.js b/forge/ee/lib/bom/tasks/cache-catalogues.js index e19c3ffe25..2167737d0b 100644 --- a/forge/ee/lib/bom/tasks/cache-catalogues.js +++ b/forge/ee/lib/bom/tasks/cache-catalogues.js @@ -38,6 +38,10 @@ module.exports = { for (const mod of modules) { const name = mod.id const version = mod.version + const current = await cache.get(name) + if (current && current !== version) { + await app.models.NodeREDNodeVersions.updateAllLatest(name, version) + } cache.set(name, version) } } diff --git a/forge/ee/lib/index.js b/forge/ee/lib/index.js index 6ce4a6e41e..e67fa332f5 100644 --- a/forge/ee/lib/index.js +++ b/forge/ee/lib/index.js @@ -61,7 +61,7 @@ module.exports = fp(async function (app, opts) { (Object.prototype.hasOwnProperty.call(app.config?.expert ?? {}, 'insights') ? !!app.config?.expert?.insights?.enabled : true) app.config.features.register('expertInsights', isInsightsEnabled ?? false, true) - + require('./bom').init(app) } From 06ac8606f113202c2dab7c3fd79c276ece74751d Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Mon, 27 Jul 2026 17:15:18 +0100 Subject: [PATCH 10/12] fix lint --- forge/ee/lib/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/ee/lib/index.js b/forge/ee/lib/index.js index e67fa332f5..e44e9179b1 100644 --- a/forge/ee/lib/index.js +++ b/forge/ee/lib/index.js @@ -61,7 +61,7 @@ module.exports = fp(async function (app, opts) { (Object.prototype.hasOwnProperty.call(app.config?.expert ?? {}, 'insights') ? !!app.config?.expert?.insights?.enabled : true) app.config.features.register('expertInsights', isInsightsEnabled ?? false, true) - + require('./bom').init(app) } From 17398ed979269561a6e3f5130d2498e62caf5541 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 28 Jul 2026 14:46:20 +0100 Subject: [PATCH 11/12] spell destroy correctly --- forge/db/models/Project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index 4df5fc40bc..69e9533d4a 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -261,7 +261,7 @@ module.exports = { } } // Remove version info when Project removed - await app.db.models.NodeREDNodeVersions.destory({ + await app.db.models.NodeREDNodeVersions.destroy({ where: { ownerType: 'instance', ownerId: project.id From e6a0dc1ac4fd9ee0e603f30b3353e424827c3dec Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Wed, 29 Jul 2026 15:45:10 +0100 Subject: [PATCH 12/12] Fix db migrations --- ...d-node-version.js => 20260729-01-add-node-red-node-version.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename forge/db/migrations/{20260717-01-add-node-red-node-version.js => 20260729-01-add-node-red-node-version.js} (100%) diff --git a/forge/db/migrations/20260717-01-add-node-red-node-version.js b/forge/db/migrations/20260729-01-add-node-red-node-version.js similarity index 100% rename from forge/db/migrations/20260717-01-add-node-red-node-version.js rename to forge/db/migrations/20260729-01-add-node-red-node-version.js