From 2b2682fb17f9f700e923a091ec09ff6a2d8e8711 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 9 Jul 2026 13:47:56 +0100 Subject: [PATCH 01/12] start of new tables --- .../20260709-01-add-node-red--node-version.js | 37 +++++++++++++ forge/db/models/NodeREDNodeVersion.js | 52 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 forge/db/migrations/20260709-01-add-node-red--node-version.js create mode 100644 forge/db/models/NodeREDNodeVersion.js diff --git a/forge/db/migrations/20260709-01-add-node-red--node-version.js b/forge/db/migrations/20260709-01-add-node-red--node-version.js new file mode 100644 index 0000000000..d43ecff603 --- /dev/null +++ b/forge/db/migrations/20260709-01-add-node-red--node-version.js @@ -0,0 +1,37 @@ +const { DataTypes } = require('sequelize') + +module.exports = { + /** + * upgrade database + * @param {QueryInterface} context Sequelize.QueryInterface + */ + up: async (context, Sequelize) => { + await context.createTable('NodeREDNodeVersions', { + ownerId: { + type: DataTypes.STRING, + allowNull: false + }, + ownerType: { + type: DataTypes.STRING, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + currentVersion: { + type: DataTypes.STRING, + allowNull: false + }, + latestVestion: { + types: DataTypes.STRING, + allowNull: true + } + }) + + await context.addIndex('NodeREDNodeVersions', { + + }) + }, + down: async (context) => {} +} \ No newline at end of file diff --git a/forge/db/models/NodeREDNodeVersion.js b/forge/db/models/NodeREDNodeVersion.js new file mode 100644 index 0000000000..97019d53d1 --- /dev/null +++ b/forge/db/models/NodeREDNodeVersion.js @@ -0,0 +1,52 @@ +/** + * 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: 'NodeREDNodeVersion', + schema: { + 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 + } + }, + indexes: [], + associations: function (M) { + this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false }) + this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false }) + }, + finders: function (M) { + return { + static: {}, + instance: {} + } + } +} \ No newline at end of file From fec1f7583173015110739164cd0cf29eed3beeb4 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 21 Jul 2026 09:59:55 +0100 Subject: [PATCH 02/12] DB tables and hooks to keep track of latest node version fixes #7736 --- ... 20260717-01-add-node-red-node-version.js} | 25 ++++++++++----- ...DNodeVersion.js => NodeREDNodeVersions.js} | 24 +++++++++++--- forge/db/models/Project.js | 8 +++++ forge/db/models/StorageSettings.js | 31 +++++++++++++++++++ forge/db/models/index.js | 3 +- forge/ee/lib/bom/tasks/cache-catalogues.js | 4 +++ 6 files changed, 81 insertions(+), 14 deletions(-) rename forge/db/migrations/{20260709-01-add-node-red--node-version.js => 20260717-01-add-node-red-node-version.js} (61%) rename forge/db/models/{NodeREDNodeVersion.js => NodeREDNodeVersions.js} (73%) diff --git a/forge/db/migrations/20260709-01-add-node-red--node-version.js b/forge/db/migrations/20260717-01-add-node-red-node-version.js similarity index 61% rename from forge/db/migrations/20260709-01-add-node-red--node-version.js rename to forge/db/migrations/20260717-01-add-node-red-node-version.js index d43ecff603..41f3aca376 100644 --- a/forge/db/migrations/20260709-01-add-node-red--node-version.js +++ b/forge/db/migrations/20260717-01-add-node-red-node-version.js @@ -7,6 +7,11 @@ module.exports = { */ up: async (context, Sequelize) => { await context.createTable('NodeREDNodeVersions', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, ownerId: { type: DataTypes.STRING, allowNull: false @@ -23,15 +28,19 @@ module.exports = { type: DataTypes.STRING, allowNull: false }, - latestVestion: { - types: DataTypes.STRING, + latestVersion: { + type: DataTypes.STRING, allowNull: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false } }) - - await context.addIndex('NodeREDNodeVersions', { - - }) }, - down: async (context) => {} -} \ No newline at end of file + down: async (context, Sequelize) => {} +} diff --git a/forge/db/models/NodeREDNodeVersion.js b/forge/db/models/NodeREDNodeVersions.js similarity index 73% rename from forge/db/models/NodeREDNodeVersion.js rename to forge/db/models/NodeREDNodeVersions.js index 97019d53d1..51601faa69 100644 --- a/forge/db/models/NodeREDNodeVersion.js +++ b/forge/db/models/NodeREDNodeVersions.js @@ -11,12 +11,17 @@ * @typedef {{name: string, schema: ModelAttributes, model: Model, indexes?: ModelIndexesOptions[], scopes?: ModelScopeOptions, options?: InitOptions}} FFModel */ -const { DataTypes } = require('sequelize'); +const { DataTypes } = require('sequelize') /** @type {FFModel} */ module.exports = { - name: 'NodeREDNodeVersion', + name: 'NodeREDNodeVersions', schema: { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, ownerId: { type: DataTypes.STRING, allowNull: false @@ -38,15 +43,24 @@ module.exports = { allowNull: true } }, - indexes: [], associations: function (M) { this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false }) this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false }) }, finders: function (M) { return { - static: {}, + static: { + updateAllLatest: async (name, version) => { + await this.update({ + latestVersion: version + }, { + where: { + name + } + }) + } + }, instance: {} } } -} \ No newline at end of file +} diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index a6dab47dfd..266d217ac9 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -260,6 +260,14 @@ module.exports = { app.log.error(`Error removing MCPRegistrations for deleted instance ${project.id}: ${err.message}`) } } + if (app.db.models.NodeREDNodeVersions?.destory) { + await app.db.models.destory({ + where: { + ownerType: 'instance', + ownerId: project.id + } + }) + } } } }, diff --git a/forge/db/models/StorageSettings.js b/forge/db/models/StorageSettings.js index dac4852517..40ee0cc769 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,34 @@ 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) + // console.log(storageSettings.ProjectId, n, nodes[n].version, latest) + await app.db.models.NodeREDNodeVersions.upsert({ + ownerId: storageSettings.ProjectId, + ownerType: 'instance', + name: n, + currentVersion: nodes[n].version, + latestVersion: latest + }) + } + } + } catch (err) { + // console.log(err) + } + } + } } } 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) } } From 279498292efdf0b154b7b7205f683e0c21c93d44 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 21 Jul 2026 10:07:57 +0100 Subject: [PATCH 03/12] remove console.log --- forge/db/models/StorageSettings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/forge/db/models/StorageSettings.js b/forge/db/models/StorageSettings.js index 40ee0cc769..96cbd32b62 100644 --- a/forge/db/models/StorageSettings.js +++ b/forge/db/models/StorageSettings.js @@ -39,7 +39,6 @@ module.exports = { for (const n of Object.keys(nodes)) { if (n !== 'node-red') { const latest = await cache.get(n) - // console.log(storageSettings.ProjectId, n, nodes[n].version, latest) await app.db.models.NodeREDNodeVersions.upsert({ ownerId: storageSettings.ProjectId, ownerType: 'instance', From 382aa7e24bd758fe989d1005591792abb078dda7 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Mon, 27 Jul 2026 16:36:36 +0100 Subject: [PATCH 04/12] Update based on feedback --- forge/db/models/Project.js | 15 +++++++-------- forge/db/models/StorageSettings.js | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index 266d217ac9..22c10f6340 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -260,14 +260,13 @@ module.exports = { app.log.error(`Error removing MCPRegistrations for deleted instance ${project.id}: ${err.message}`) } } - if (app.db.models.NodeREDNodeVersions?.destory) { - await app.db.models.destory({ - where: { - ownerType: 'instance', - ownerId: project.id - } - }) - } + // 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 96cbd32b62..10a62bb420 100644 --- a/forge/db/models/StorageSettings.js +++ b/forge/db/models/StorageSettings.js @@ -49,7 +49,7 @@ module.exports = { } } } catch (err) { - // console.log(err) + // swallowing error as this is an async hook triggered by an update to the underlying table } } } 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 74668037b3787e06a8b714016592396777119105 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Thu, 9 Jul 2026 13:47:56 +0100 Subject: [PATCH 09/12] start of new tables --- .../20260709-01-add-node-red--node-version.js | 37 +++++++++++++ forge/db/models/NodeREDNodeVersion.js | 52 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 forge/db/migrations/20260709-01-add-node-red--node-version.js create mode 100644 forge/db/models/NodeREDNodeVersion.js diff --git a/forge/db/migrations/20260709-01-add-node-red--node-version.js b/forge/db/migrations/20260709-01-add-node-red--node-version.js new file mode 100644 index 0000000000..d43ecff603 --- /dev/null +++ b/forge/db/migrations/20260709-01-add-node-red--node-version.js @@ -0,0 +1,37 @@ +const { DataTypes } = require('sequelize') + +module.exports = { + /** + * upgrade database + * @param {QueryInterface} context Sequelize.QueryInterface + */ + up: async (context, Sequelize) => { + await context.createTable('NodeREDNodeVersions', { + ownerId: { + type: DataTypes.STRING, + allowNull: false + }, + ownerType: { + type: DataTypes.STRING, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + currentVersion: { + type: DataTypes.STRING, + allowNull: false + }, + latestVestion: { + types: DataTypes.STRING, + allowNull: true + } + }) + + await context.addIndex('NodeREDNodeVersions', { + + }) + }, + down: async (context) => {} +} \ No newline at end of file diff --git a/forge/db/models/NodeREDNodeVersion.js b/forge/db/models/NodeREDNodeVersion.js new file mode 100644 index 0000000000..97019d53d1 --- /dev/null +++ b/forge/db/models/NodeREDNodeVersion.js @@ -0,0 +1,52 @@ +/** + * 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: 'NodeREDNodeVersion', + schema: { + 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 + } + }, + indexes: [], + associations: function (M) { + this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false }) + this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false }) + }, + finders: function (M) { + return { + static: {}, + instance: {} + } + } +} \ No newline at end of file From 2d0a58d5c3c705184d3d061493bb74d5b919ab35 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 21 Jul 2026 09:59:55 +0100 Subject: [PATCH 10/12] DB tables and hooks to keep track of latest node version fixes #7736 --- ... 20260717-01-add-node-red-node-version.js} | 25 ++++++++++----- ...DNodeVersion.js => NodeREDNodeVersions.js} | 24 +++++++++++--- forge/db/models/Project.js | 8 +++++ forge/db/models/StorageSettings.js | 31 +++++++++++++++++++ forge/db/models/index.js | 3 +- forge/ee/lib/bom/tasks/cache-catalogues.js | 4 +++ 6 files changed, 81 insertions(+), 14 deletions(-) rename forge/db/migrations/{20260709-01-add-node-red--node-version.js => 20260717-01-add-node-red-node-version.js} (61%) rename forge/db/models/{NodeREDNodeVersion.js => NodeREDNodeVersions.js} (73%) diff --git a/forge/db/migrations/20260709-01-add-node-red--node-version.js b/forge/db/migrations/20260717-01-add-node-red-node-version.js similarity index 61% rename from forge/db/migrations/20260709-01-add-node-red--node-version.js rename to forge/db/migrations/20260717-01-add-node-red-node-version.js index d43ecff603..41f3aca376 100644 --- a/forge/db/migrations/20260709-01-add-node-red--node-version.js +++ b/forge/db/migrations/20260717-01-add-node-red-node-version.js @@ -7,6 +7,11 @@ module.exports = { */ up: async (context, Sequelize) => { await context.createTable('NodeREDNodeVersions', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, ownerId: { type: DataTypes.STRING, allowNull: false @@ -23,15 +28,19 @@ module.exports = { type: DataTypes.STRING, allowNull: false }, - latestVestion: { - types: DataTypes.STRING, + latestVersion: { + type: DataTypes.STRING, allowNull: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false } }) - - await context.addIndex('NodeREDNodeVersions', { - - }) }, - down: async (context) => {} -} \ No newline at end of file + down: async (context, Sequelize) => {} +} diff --git a/forge/db/models/NodeREDNodeVersion.js b/forge/db/models/NodeREDNodeVersions.js similarity index 73% rename from forge/db/models/NodeREDNodeVersion.js rename to forge/db/models/NodeREDNodeVersions.js index 97019d53d1..51601faa69 100644 --- a/forge/db/models/NodeREDNodeVersion.js +++ b/forge/db/models/NodeREDNodeVersions.js @@ -11,12 +11,17 @@ * @typedef {{name: string, schema: ModelAttributes, model: Model, indexes?: ModelIndexesOptions[], scopes?: ModelScopeOptions, options?: InitOptions}} FFModel */ -const { DataTypes } = require('sequelize'); +const { DataTypes } = require('sequelize') /** @type {FFModel} */ module.exports = { - name: 'NodeREDNodeVersion', + name: 'NodeREDNodeVersions', schema: { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, ownerId: { type: DataTypes.STRING, allowNull: false @@ -38,15 +43,24 @@ module.exports = { allowNull: true } }, - indexes: [], associations: function (M) { this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false }) this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false }) }, finders: function (M) { return { - static: {}, + static: { + updateAllLatest: async (name, version) => { + await this.update({ + latestVersion: version + }, { + where: { + name + } + }) + } + }, instance: {} } } -} \ No newline at end of file +} diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index 3ca0e70653..060d71c24e 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -260,6 +260,14 @@ module.exports = { app.log.error(`Error removing MCPRegistrations for deleted instance ${project.id}: ${err.message}`) } } + if (app.db.models.NodeREDNodeVersions?.destory) { + await app.db.models.destory({ + where: { + ownerType: 'instance', + ownerId: project.id + } + }) + } } } }, diff --git a/forge/db/models/StorageSettings.js b/forge/db/models/StorageSettings.js index dac4852517..40ee0cc769 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,34 @@ 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) + // console.log(storageSettings.ProjectId, n, nodes[n].version, latest) + await app.db.models.NodeREDNodeVersions.upsert({ + ownerId: storageSettings.ProjectId, + ownerType: 'instance', + name: n, + currentVersion: nodes[n].version, + latestVersion: latest + }) + } + } + } catch (err) { + // console.log(err) + } + } + } } } 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) } } From 28d2221f0827016b6672c2e635ac12b753cbdf89 Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Tue, 21 Jul 2026 10:07:57 +0100 Subject: [PATCH 11/12] remove console.log --- forge/db/models/StorageSettings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/forge/db/models/StorageSettings.js b/forge/db/models/StorageSettings.js index 40ee0cc769..96cbd32b62 100644 --- a/forge/db/models/StorageSettings.js +++ b/forge/db/models/StorageSettings.js @@ -39,7 +39,6 @@ module.exports = { for (const n of Object.keys(nodes)) { if (n !== 'node-red') { const latest = await cache.get(n) - // console.log(storageSettings.ProjectId, n, nodes[n].version, latest) await app.db.models.NodeREDNodeVersions.upsert({ ownerId: storageSettings.ProjectId, ownerType: 'instance', From f3f377d11ffd321e5d1c5cc0f61c28f82e4a47fa Mon Sep 17 00:00:00 2001 From: Ben Hardill Date: Mon, 27 Jul 2026 16:36:36 +0100 Subject: [PATCH 12/12] Update based on feedback --- forge/db/models/Project.js | 15 +++++++-------- forge/db/models/StorageSettings.js | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/forge/db/models/Project.js b/forge/db/models/Project.js index 060d71c24e..246bcb97f5 100644 --- a/forge/db/models/Project.js +++ b/forge/db/models/Project.js @@ -260,14 +260,13 @@ module.exports = { app.log.error(`Error removing MCPRegistrations for deleted instance ${project.id}: ${err.message}`) } } - if (app.db.models.NodeREDNodeVersions?.destory) { - await app.db.models.destory({ - where: { - ownerType: 'instance', - ownerId: project.id - } - }) - } + // 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 96cbd32b62..10a62bb420 100644 --- a/forge/db/models/StorageSettings.js +++ b/forge/db/models/StorageSettings.js @@ -49,7 +49,7 @@ module.exports = { } } } catch (err) { - // console.log(err) + // swallowing error as this is an async hook triggered by an update to the underlying table } } }