From 485e48bdd1df9afdbd3dc0930c429345a207a853 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 07:01:44 +0100 Subject: [PATCH 1/7] feat(api): add migration script v2.2.9 --- .../1748492346868-v-2-2-9.migration.ts | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts new file mode 100644 index 00000000..ee47afe5 --- /dev/null +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -0,0 +1,170 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import mongoose from 'mongoose'; + +import blockSchema, { Block } from '@/chat/schemas/block.schema'; +import roleSchema, { Role } from '@/user/schemas/role.schema'; +import userSchema, { User } from '@/user/schemas/user.schema'; + +import { MigrationServices } from '../types'; + +/** + * @returns The admin user or null + */ +const getAdminUser = async () => { + const RoleModel = mongoose.model(Role.name, roleSchema); + const UserModel = mongoose.model(User.name, userSchema); + + const adminRole = await RoleModel.findOne({ name: 'admin' }); + const user = await UserModel.findOne({ roles: [adminRole!._id] }).sort({ + createdAt: 'asc', + }); + + return user!; +}; + +const migrateBlockOptionsContentLimit = async ({ + logger, +}: MigrationServices) => { + const BlockModel = mongoose.model(Block.name, blockSchema); + + // Find blocks where "options.content.limit" exists and has string type + const cursor = BlockModel.find({ + 'options.content.limit': { $exists: true, $type: 'string' }, + }).cursor(); + + const updateBlockOptionsContentLimit = async ( + blockId: mongoose.Types.ObjectId, + limit: string | number, + ) => { + await BlockModel.updateOne( + { _id: blockId }, + { $set: { 'options.content.limit': limit } }, + ); + }; + + const getBlockOptionsContentLimitDefaultValue = (block: Block): number => { + return block.options.content?.display === 'list' ? 1 : 2; + }; + + const adminUser = await getAdminUser(); + + if (!adminUser) { + logger.warn('Unable to process block, no admin user found'); + return; + } + + for await (const block of cursor) { + try { + if (block.options.content && 'limit' in block.options.content) { + const limitDefaultValue = + getBlockOptionsContentLimitDefaultValue(block); + const newLimitValue = + block.options.content.limit > 0 + ? parseInt(block.options.content.limit.toString()) + : limitDefaultValue; + + await updateBlockOptionsContentLimit(block._id, newLimitValue); + } else { + throw new Error('Unable to process the block update'); + } + } catch (error) { + logger.error( + `Failed to update limit ${block._id}: ${error.message}, defaulting limit to 2`, + ); + + try { + await updateBlockOptionsContentLimit(block._id, 2); + } catch (err) { + logger.error( + `Failed to update limit ${block._id}: ${error.message}, unable to default to 2`, + ); + } + } + } +}; + +const migrateBlockOptionsContentButtonsUrl = async ({ + logger, +}: MigrationServices) => { + const BlockModel = mongoose.model(Block.name, blockSchema); + + const cursor = BlockModel.find({ + $or: [ + { 'options.content.buttons.url': { $exists: false } }, + { 'options.content.buttons.url': false }, + ], + }).cursor(); + + for await (const block of cursor) { + try { + await BlockModel.updateOne( + { _id: block.id }, + { + $set: { + 'options.content.buttons.$[].url': '', + }, + }, + ); + } catch (error) { + logger.error( + `Failed to update button url ${block._id}: ${error.message}`, + ); + } + } +}; + +const migrateBlockOptionsFallback = async ({ logger }: MigrationServices) => { + const BlockModel = mongoose.model(Block.name, blockSchema); + + const cursor = BlockModel.find({ + 'options.fallback.max_attempts': { $exists: true }, + }).cursor(); + + for await (const block of cursor) { + try { + if (block.options.fallback?.message.length === 0) { + await BlockModel.updateOne( + { _id: block.id }, + { + $set: { + 'options.fallback.max_attempts': 0, + 'options.fallback.active': false, + }, + }, + ); + } else { + await BlockModel.updateOne( + { _id: block.id }, + { + $set: { + 'options.fallback.max_attempts': parseInt( + (block.options.fallback?.max_attempts || 0).toString(), + ), + }, + }, + ); + } + } catch (error) { + logger.error(`Failed to update fallback ${error.message}`); + } + } +}; + +module.exports = { + async up(services: MigrationServices) { + await migrateBlockOptionsContentLimit(services); + await migrateBlockOptionsContentButtonsUrl(services); + await migrateBlockOptionsFallback(services); + return true; + }, + async down(_services: MigrationServices) { + return true; + }, +}; From 64a8729be34b2247747518350e365ec67095d1d7 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 08:15:23 +0100 Subject: [PATCH 2/7] fix(api): apply feedback --- .../1748492346868-v-2-2-9.migration.ts | 168 +++++++----------- 1 file changed, 63 insertions(+), 105 deletions(-) diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts index ee47afe5..c1450d54 100644 --- a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -29,131 +29,89 @@ const getAdminUser = async () => { return user!; }; -const migrateBlockOptionsContentLimit = async ({ - logger, -}: MigrationServices) => { +const migrateBlockOptionsContentLimit = async (services: MigrationServices) => { const BlockModel = mongoose.model(Block.name, blockSchema); - // Find blocks where "options.content.limit" exists and has string type - const cursor = BlockModel.find({ - 'options.content.limit': { $exists: true, $type: 'string' }, - }).cursor(); - - const updateBlockOptionsContentLimit = async ( - blockId: mongoose.Types.ObjectId, - limit: string | number, - ) => { - await BlockModel.updateOne( - { _id: blockId }, - { $set: { 'options.content.limit': limit } }, - ); - }; - - const getBlockOptionsContentLimitDefaultValue = (block: Block): number => { - return block.options.content?.display === 'list' ? 1 : 2; - }; - const adminUser = await getAdminUser(); if (!adminUser) { - logger.warn('Unable to process block, no admin user found'); + services.logger.warn('Unable to process block, no admin user found'); return; } - for await (const block of cursor) { - try { - if (block.options.content && 'limit' in block.options.content) { - const limitDefaultValue = - getBlockOptionsContentLimitDefaultValue(block); - const newLimitValue = - block.options.content.limit > 0 - ? parseInt(block.options.content.limit.toString()) - : limitDefaultValue; - - await updateBlockOptionsContentLimit(block._id, newLimitValue); - } else { - throw new Error('Unable to process the block update'); - } - } catch (error) { - logger.error( - `Failed to update limit ${block._id}: ${error.message}, defaulting limit to 2`, - ); - - try { - await updateBlockOptionsContentLimit(block._id, 2); - } catch (err) { - logger.error( - `Failed to update limit ${block._id}: ${error.message}, unable to default to 2`, - ); - } - } - } -}; - -const migrateBlockOptionsContentButtonsUrl = async ({ - logger, -}: MigrationServices) => { - const BlockModel = mongoose.model(Block.name, blockSchema); - - const cursor = BlockModel.find({ - $or: [ - { 'options.content.buttons.url': { $exists: false } }, - { 'options.content.buttons.url': false }, - ], - }).cursor(); - - for await (const block of cursor) { - try { - await BlockModel.updateOne( - { _id: block.id }, + try { + await BlockModel.updateMany( + { 'options.content.limit': { $exists: true } }, + [ { $set: { - 'options.content.buttons.$[].url': '', + 'options.content.limit': { $toInt: '$options.content.limit' }, }, }, - ); - } catch (error) { - logger.error( - `Failed to update button url ${block._id}: ${error.message}`, - ); - } + ], + ); + } catch (error) { + services.logger.error(`Failed to update limit : ${error.message}`); } }; -const migrateBlockOptionsFallback = async ({ logger }: MigrationServices) => { +const migrateBlockOptionsContentButtonsUrl = async ( + services: MigrationServices, +) => { const BlockModel = mongoose.model(Block.name, blockSchema); - const cursor = BlockModel.find({ - 'options.fallback.max_attempts': { $exists: true }, - }).cursor(); + try { + await BlockModel.updateMany( + { + $or: [ + { 'options.content.buttons.url': { $exists: false } }, + { 'options.content.buttons.url': false }, + ], + }, + { + $set: { + 'options.content.buttons.$[].url': '', + }, + }, + ); + } catch (error) { + services.logger.error(`Failed to update button url : ${error.message}`); + } +}; - for await (const block of cursor) { - try { - if (block.options.fallback?.message.length === 0) { - await BlockModel.updateOne( - { _id: block.id }, - { - $set: { - 'options.fallback.max_attempts': 0, - 'options.fallback.active': false, +const migrateBlockOptionsFallback = async (services: MigrationServices) => { + const BlockModel = mongoose.model(Block.name, blockSchema); + + try { + await BlockModel.updateMany( + { 'options.fallback.max_attempts': { $exists: true } }, + [ + { + $set: { + 'options.fallback.max_attempts': { + $toInt: '$options.fallback.max_attempts', }, }, - ); - } else { - await BlockModel.updateOne( - { _id: block.id }, - { - $set: { - 'options.fallback.max_attempts': parseInt( - (block.options.fallback?.max_attempts || 0).toString(), - ), - }, - }, - ); - } - } catch (error) { - logger.error(`Failed to update fallback ${error.message}`); - } + }, + ], + ); + } catch (error) { + services.logger.error(`Failed to update max_attempts : ${error.message}`); + } + + try { + await BlockModel.updateMany({ 'options.fallback.message': { $size: 0 } }, [ + { + $set: { + 'options.fallback.max_attempts': 0, + 'options.fallback.active': false, + }, + }, + ]); + } catch (error) { + services.logger.error( + `Failed to update max_attempts, active : ${error.message}`, + ); } }; From 772236d220f0fbdbe176ebe2c7ddd0ceec22128f Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 09:12:51 +0100 Subject: [PATCH 3/7] fix(api): apply feedback --- .../1748492346868-v-2-2-9.migration.ts | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts index c1450d54..cf8da6bc 100644 --- a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -30,13 +30,12 @@ const getAdminUser = async () => { }; const migrateBlockOptionsContentLimit = async (services: MigrationServices) => { - const BlockModel = mongoose.model(Block.name, blockSchema); + const BlockModel = mongoose.model(Block.name, blockSchema).collection; const adminUser = await getAdminUser(); if (!adminUser) { services.logger.warn('Unable to process block, no admin user found'); - return; } try { @@ -52,13 +51,16 @@ const migrateBlockOptionsContentLimit = async (services: MigrationServices) => { ); } catch (error) { services.logger.error(`Failed to update limit : ${error.message}`); + + return false; } + return true; }; const migrateBlockOptionsContentButtonsUrl = async ( services: MigrationServices, ) => { - const BlockModel = mongoose.model(Block.name, blockSchema); + const BlockModel = mongoose.model(Block.name, blockSchema).collection; try { await BlockModel.updateMany( @@ -76,15 +78,18 @@ const migrateBlockOptionsContentButtonsUrl = async ( ); } catch (error) { services.logger.error(`Failed to update button url : ${error.message}`); + + return false; } + return true; }; const migrateBlockOptionsFallback = async (services: MigrationServices) => { - const BlockModel = mongoose.model(Block.name, blockSchema); + const BlockModel = mongoose.model(Block.name, blockSchema).collection; try { await BlockModel.updateMany( - { 'options.fallback.max_attempts': { $exists: true } }, + { 'options.fallback.max_attempts': { $exists: true, type: 'string' } }, [ { $set: { @@ -97,6 +102,7 @@ const migrateBlockOptionsFallback = async (services: MigrationServices) => { ); } catch (error) { services.logger.error(`Failed to update max_attempts : ${error.message}`); + return false; } try { @@ -112,15 +118,18 @@ const migrateBlockOptionsFallback = async (services: MigrationServices) => { services.logger.error( `Failed to update max_attempts, active : ${error.message}`, ); + return false; } + return true; }; module.exports = { async up(services: MigrationServices) { - await migrateBlockOptionsContentLimit(services); - await migrateBlockOptionsContentButtonsUrl(services); - await migrateBlockOptionsFallback(services); - return true; + const migration1 = await migrateBlockOptionsContentLimit(services); + const migration2 = await migrateBlockOptionsContentButtonsUrl(services); + const migration3 = await migrateBlockOptionsFallback(services); + + return migration1 && migration2 && migration3; }, async down(_services: MigrationServices) { return true; From 3a4cc3434a4541feafb50d4e9a4574952eb81290 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 15:43:40 +0100 Subject: [PATCH 4/7] fix(api): apply feedback --- .../migrations/1748492346868-v-2-2-9.migration.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts index cf8da6bc..0c9051fb 100644 --- a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -125,11 +125,15 @@ const migrateBlockOptionsFallback = async (services: MigrationServices) => { module.exports = { async up(services: MigrationServices) { - const migration1 = await migrateBlockOptionsContentLimit(services); - const migration2 = await migrateBlockOptionsContentButtonsUrl(services); - const migration3 = await migrateBlockOptionsFallback(services); + try { + await migrateBlockOptionsContentLimit(services); + await migrateBlockOptionsContentButtonsUrl(services); + await migrateBlockOptionsFallback(services); - return migration1 && migration2 && migration3; + return true; + } catch (err) { + return false; + } }, async down(_services: MigrationServices) { return true; From 04fcc1585e724d18326ae5f71796b75d4b22f972 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 16:04:41 +0100 Subject: [PATCH 5/7] fix: enhance logic --- .../migrations/1748492346868-v-2-2-9.migration.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts index 0c9051fb..1dab6c02 100644 --- a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -64,12 +64,7 @@ const migrateBlockOptionsContentButtonsUrl = async ( try { await BlockModel.updateMany( - { - $or: [ - { 'options.content.buttons.url': { $exists: false } }, - { 'options.content.buttons.url': false }, - ], - }, + { 'options.content.buttons.url': false }, { $set: { 'options.content.buttons.$[].url': '', @@ -89,7 +84,7 @@ const migrateBlockOptionsFallback = async (services: MigrationServices) => { try { await BlockModel.updateMany( - { 'options.fallback.max_attempts': { $exists: true, type: 'string' } }, + { 'options.fallback.max_attempts': { $exists: true, $type: 'string' } }, [ { $set: { From 8c9c5141ab88f7ef56a8b7bc111c60770439a146 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 17:00:00 +0100 Subject: [PATCH 6/7] fix: adapt not found admin case --- api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts index 1dab6c02..080c9e87 100644 --- a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -36,6 +36,7 @@ const migrateBlockOptionsContentLimit = async (services: MigrationServices) => { if (!adminUser) { services.logger.warn('Unable to process block, no admin user found'); + return; } try { From 8370ca692c3dfbcefb3bc54079e0d7bc573e18ea Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Thu, 29 May 2025 17:37:34 +0100 Subject: [PATCH 7/7] fix: add throw propagation --- .../1748492346868-v-2-2-9.migration.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts index 080c9e87..ad3190a7 100644 --- a/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts +++ b/api/src/migration/migrations/1748492346868-v-2-2-9.migration.ts @@ -53,9 +53,8 @@ const migrateBlockOptionsContentLimit = async (services: MigrationServices) => { } catch (error) { services.logger.error(`Failed to update limit : ${error.message}`); - return false; + throw error instanceof Error ? error : new Error(error); } - return true; }; const migrateBlockOptionsContentButtonsUrl = async ( @@ -75,9 +74,8 @@ const migrateBlockOptionsContentButtonsUrl = async ( } catch (error) { services.logger.error(`Failed to update button url : ${error.message}`); - return false; + throw error instanceof Error ? error : new Error(error); } - return true; }; const migrateBlockOptionsFallback = async (services: MigrationServices) => { @@ -98,7 +96,7 @@ const migrateBlockOptionsFallback = async (services: MigrationServices) => { ); } catch (error) { services.logger.error(`Failed to update max_attempts : ${error.message}`); - return false; + throw error instanceof Error ? error : new Error(error); } try { @@ -114,9 +112,8 @@ const migrateBlockOptionsFallback = async (services: MigrationServices) => { services.logger.error( `Failed to update max_attempts, active : ${error.message}`, ); - return false; + throw error instanceof Error ? error : new Error(error); } - return true; }; module.exports = { @@ -127,8 +124,9 @@ module.exports = { await migrateBlockOptionsFallback(services); return true; - } catch (err) { - return false; + } catch (error) { + services.logger.error(`Migration failed : ${error.message}`); + throw error instanceof Error ? error : new Error(error); } }, async down(_services: MigrationServices) {