From 78db148150b5a76932bb7b1048c23dff0a2774c3 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 14 Jul 2025 14:56:11 +0300 Subject: [PATCH] Cleanup and add safe utility functions (#154) * Cleanup and add safe utility functions * Fix npm command * Update version * Fix an issue with dist builds caching * Update version --- Dockerfile | 11 +-- backend/cmd/start.sh | 4 +- .../20250618000001-enhance-recurring-tasks.js | 13 +--- .../20250619000001-add-recurring-parent-id.js | 7 +- .../20250619000002-add-project-image-url.js | 17 +++-- ...620000001-add-task-intelligence-enabled.js | 19 +++-- ...2-add-auto-suggest-next-actions-enabled.js | 21 +++--- ...0250621221841-add-completed-at-to-tasks.js | 24 +++--- .../20250621223000-create-calendar-tokens.js | 5 +- .../20250622000001-create-task-events.js | 39 +++------- ...622053925-add-pomodoro-enabled-to-users.js | 20 +++-- .../20250623000001-add-uuid-to-tasks.js | 23 +++--- .../20250623000003-create-notes-tags-table.js | 3 +- ...0623000004-add-timestamps-to-notes-tags.js | 40 +++++----- ...3000005-add-timestamps-to-projects-tags.js | 50 ++++++------- ...-add-suggestion-metadata-to-inbox-items.js | 73 +++++++++++-------- ...2131-add-productivity-assistant-columns.js | 6 +- ...20250713233301-add-title-to-inbox-items.js | 23 ++++++ backend/package.json | 6 +- backend/utils/migration-utils.js | 62 ++++++++++++++++ package.json | 2 +- 21 files changed, 285 insertions(+), 183 deletions(-) create mode 100644 backend/migrations/20250713233301-add-title-to-inbox-items.js create mode 100644 backend/utils/migration-utils.js diff --git a/Dockerfile b/Dockerfile index dc15ce2..64debee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,17 +50,18 @@ RUN apk add --no-cache --virtual .runtime-deps \ # Set working directory WORKDIR /app +# Copy backend +COPY ./backend/ /app/backend/ +RUN chmod +x /app/backend/cmd/start.sh + # Copy frontend +RUN rm -rf /app/backend/dist COPY --from=builder --chown=app:app /app/dist ./backend/dist COPY --from=builder --chown=app:app /app/public/locales ./backend/dist/locales # Copy backend dependencies COPY --from=builder --chown=app:app /app/backend/node_modules ./backend/node_modules -# Copy backend -COPY ./backend/ /app/backend/ -RUN chmod +x /app/backend/cmd/start.sh - # Create necessary directories RUN mkdir -p /app/backend/db /app/backend/certs && \ chown -R app:app /app @@ -94,4 +95,4 @@ HEALTHCHECK --interval=60s --timeout=3s --start-period=10s --retries=2 \ # Use dumb-init for proper signal handling ENTRYPOINT ["dumb-init", "--"] WORKDIR /app/backend -CMD ["/app/backend/cmd/start.sh"] +CMD ["/app/backend/cmd/start.sh"] \ No newline at end of file diff --git a/backend/cmd/start.sh b/backend/cmd/start.sh index d763539..d3a6bce 100755 --- a/backend/cmd/start.sh +++ b/backend/cmd/start.sh @@ -44,10 +44,10 @@ else echo "⚠️ Migration failed, but continuing startup (may be expected for new installations)" fi -if [ -n "$TUDUDI_USER_EMAIL" ] && [ -n "$TUDUDI_USER_PASSWORD" ]; then +if [ -n "${TUDUDI_USER_EMAIL:-}" ] && [ -n "${TUDUDI_USER_PASSWORD:-}" ]; then node -e "const{User}=require(\"./models\");const bcrypt=require(\"bcrypt\");(async()=>{try{const[u,c]=await User.findOrCreate({where:{email:process.env.TUDUDI_USER_EMAIL},defaults:{email:process.env.TUDUDI_USER_EMAIL,password_digest:await bcrypt.hash(process.env.TUDUDI_USER_PASSWORD,10)}});console.log(c?\"✅ User created\":\"ℹ️ User exists\");process.exit(0)}catch(e){console.error(\"❌\",e.message);process.exit(1)}})();" || exit 1 fi -[ "$TUDUDI_INTERNAL_SSL_ENABLED" = "true" ] && [ ! -f "certs/server.crt" ] && openssl req -x509 -newkey rsa:2048 -keyout certs/server.key -out certs/server.crt -days 365 -nodes -subj "/CN=localhost" 2>/dev/null || true +[ "${TUDUDI_INTERNAL_SSL_ENABLED:-}" = "true" ] && [ ! -f "certs/server.crt" ] && openssl req -x509 -newkey rsa:2048 -keyout certs/server.key -out certs/server.crt -days 365 -nodes -subj "/CN=localhost" 2>/dev/null || true exec node app.js diff --git a/backend/migrations/20250618000001-enhance-recurring-tasks.js b/backend/migrations/20250618000001-enhance-recurring-tasks.js index bdabc8f..038c25c 100644 --- a/backend/migrations/20250618000001-enhance-recurring-tasks.js +++ b/backend/migrations/20250618000001-enhance-recurring-tasks.js @@ -3,10 +3,8 @@ module.exports = { async up(queryInterface, Sequelize) { try { - // Get current table schema const tableInfo = await queryInterface.describeTable('tasks'); - // Define columns to add const columnsToAdd = [ { name: 'recurrence_weekday', @@ -22,8 +20,7 @@ module.exports = { definition: { type: Sequelize.INTEGER, allowNull: true, - comment: - 'Day of month (1-31) for monthly recurrence, -1 for last day', + comment: 'Day of month (1-31) for monthly recurrence', }, }, { @@ -32,7 +29,7 @@ module.exports = { type: Sequelize.INTEGER, allowNull: true, comment: - 'Week of month (1-5) for monthly weekday recurrence', + 'Week of month (1-4, -1=last) for monthly recurrence', }, }, { @@ -47,7 +44,6 @@ module.exports = { }, ]; - // Add only missing columns for (const column of columnsToAdd) { if (!(column.name in tableInfo)) { await queryInterface.addColumn( @@ -58,7 +54,6 @@ module.exports = { } } - // Add index if it doesn't exist try { const indexes = await queryInterface.showIndex('tasks'); const indexExists = indexes.some( @@ -76,7 +71,7 @@ module.exports = { } } catch (indexError) { console.log( - 'Could not check or add index:', + 'Could not check or add index for recurrence lookup:', indexError.message ); } @@ -87,13 +82,11 @@ module.exports = { }, async down(queryInterface, Sequelize) { - // Remove the added columns await queryInterface.removeColumn('tasks', 'recurrence_weekday'); await queryInterface.removeColumn('tasks', 'recurrence_month_day'); await queryInterface.removeColumn('tasks', 'recurrence_week_of_month'); await queryInterface.removeColumn('tasks', 'completion_based'); - // Remove the index await queryInterface.removeIndex( 'tasks', 'idx_tasks_recurrence_lookup' diff --git a/backend/migrations/20250619000001-add-recurring-parent-id.js b/backend/migrations/20250619000001-add-recurring-parent-id.js index 0f7d8b0..419e6ed 100644 --- a/backend/migrations/20250619000001-add-recurring-parent-id.js +++ b/backend/migrations/20250619000001-add-recurring-parent-id.js @@ -3,10 +3,8 @@ module.exports = { up: async (queryInterface, Sequelize) => { try { - // Get current table schema const tableInfo = await queryInterface.describeTable('tasks'); - // Add column if it doesn't exist if (!('recurring_parent_id' in tableInfo)) { await queryInterface.addColumn('tasks', 'recurring_parent_id', { type: Sequelize.INTEGER, @@ -20,7 +18,6 @@ module.exports = { }); } - // Add index if it doesn't exist try { const indexes = await queryInterface.showIndex('tasks'); const indexExists = indexes.some((index) => @@ -46,8 +43,8 @@ module.exports = { } }, - down: async (queryInterface, Sequelize) => { + down: async (queryInterface) => { await queryInterface.removeIndex('tasks', ['recurring_parent_id']); await queryInterface.removeColumn('tasks', 'recurring_parent_id'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250619000002-add-project-image-url.js b/backend/migrations/20250619000002-add-project-image-url.js index 9d59fe5..35270d6 100644 --- a/backend/migrations/20250619000002-add-project-image-url.js +++ b/backend/migrations/20250619000002-add-project-image-url.js @@ -1,14 +1,21 @@ 'use strict'; +const { safeAddColumns } = require('../utils/migration-utils'); + module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.addColumn('projects', 'image_url', { - type: Sequelize.TEXT, - allowNull: true, - }); + await safeAddColumns(queryInterface, 'projects', [ + { + name: 'image_url', + definition: { + type: Sequelize.TEXT, + allowNull: true, + }, + }, + ]); }, down: async (queryInterface, Sequelize) => { await queryInterface.removeColumn('projects', 'image_url'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250620000001-add-task-intelligence-enabled.js b/backend/migrations/20250620000001-add-task-intelligence-enabled.js index d867a94..2e49d8d 100644 --- a/backend/migrations/20250620000001-add-task-intelligence-enabled.js +++ b/backend/migrations/20250620000001-add-task-intelligence-enabled.js @@ -1,15 +1,22 @@ 'use strict'; +const { safeAddColumns } = require('../utils/migration-utils'); + module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.addColumn('users', 'task_intelligence_enabled', { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: true, - }); + await safeAddColumns(queryInterface, 'users', [ + { + name: 'task_intelligence_enabled', + definition: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + }, + ]); }, down: async (queryInterface, Sequelize) => { await queryInterface.removeColumn('users', 'task_intelligence_enabled'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250620000002-add-auto-suggest-next-actions-enabled.js b/backend/migrations/20250620000002-add-auto-suggest-next-actions-enabled.js index 67c3d25..b8bc796 100644 --- a/backend/migrations/20250620000002-add-auto-suggest-next-actions-enabled.js +++ b/backend/migrations/20250620000002-add-auto-suggest-next-actions-enabled.js @@ -1,16 +1,19 @@ 'use strict'; +const { safeAddColumns } = require('../utils/migration-utils'); + module.exports = { up: async (queryInterface, Sequelize) => { - await queryInterface.addColumn( - 'users', - 'auto_suggest_next_actions_enabled', + await safeAddColumns(queryInterface, 'users', [ { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: false, - } - ); + name: 'auto_suggest_next_actions_enabled', + definition: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }, + }, + ]); }, down: async (queryInterface, Sequelize) => { @@ -19,4 +22,4 @@ module.exports = { 'auto_suggest_next_actions_enabled' ); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250621221841-add-completed-at-to-tasks.js b/backend/migrations/20250621221841-add-completed-at-to-tasks.js index 00e420f..c9b751b 100644 --- a/backend/migrations/20250621221841-add-completed-at-to-tasks.js +++ b/backend/migrations/20250621221841-add-completed-at-to-tasks.js @@ -1,22 +1,24 @@ 'use strict'; +const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils'); + module.exports = { async up(queryInterface, Sequelize) { - // Add completed_at column to tasks table - await queryInterface.addColumn('tasks', 'completed_at', { - type: Sequelize.DATE, - allowNull: true, - }); + await safeAddColumns(queryInterface, 'tasks', [ + { + name: 'completed_at', + definition: { + type: Sequelize.DATE, + allowNull: true, + }, + }, + ]); - // Add an index for better query performance - await queryInterface.addIndex('tasks', ['completed_at']); + await safeAddIndex(queryInterface, 'tasks', ['completed_at']); }, async down(queryInterface, Sequelize) { - // Remove the index first await queryInterface.removeIndex('tasks', ['completed_at']); - - // Remove the completed_at column await queryInterface.removeColumn('tasks', 'completed_at'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250621223000-create-calendar-tokens.js b/backend/migrations/20250621223000-create-calendar-tokens.js index 691eae3..836bd3f 100644 --- a/backend/migrations/20250621223000-create-calendar-tokens.js +++ b/backend/migrations/20250621223000-create-calendar-tokens.js @@ -33,6 +33,7 @@ module.exports = { }, token_type: { type: Sequelize.STRING, + allowNull: true, defaultValue: 'Bearer', }, expires_at: { @@ -59,14 +60,12 @@ module.exports = { }, }); - // Add unique index for user_id + provider combination await queryInterface.addIndex('calendar_tokens', { fields: ['user_id', 'provider'], unique: true, name: 'calendar_tokens_user_provider_unique', }); - // Add index for faster lookups by user_id await queryInterface.addIndex('calendar_tokens', { fields: ['user_id'], name: 'calendar_tokens_user_id_index', @@ -76,4 +75,4 @@ module.exports = { down: async (queryInterface, Sequelize) => { await queryInterface.dropTable('calendar_tokens'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250622000001-create-task-events.js b/backend/migrations/20250622000001-create-task-events.js index 1d964be..0d03f1d 100644 --- a/backend/migrations/20250622000001-create-task-events.js +++ b/backend/migrations/20250622000001-create-task-events.js @@ -1,9 +1,10 @@ 'use strict'; +const { safeCreateTable, safeAddIndex } = require('../utils/migration-utils'); + module.exports = { async up(queryInterface, Sequelize) { - // Create task_events table - await queryInterface.createTable('task_events', { + await safeCreateTable(queryInterface, 'task_events', { id: { type: Sequelize.INTEGER, primaryKey: true, @@ -33,29 +34,22 @@ module.exports = { event_type: { type: Sequelize.STRING, allowNull: false, - // Common event types: 'created', 'status_changed', 'priority_changed', - // 'due_date_changed', 'project_changed', 'name_changed', 'description_changed', - // 'completed', 'archived', 'deleted', 'restored' }, old_value: { type: Sequelize.TEXT, allowNull: true, - // JSON string of the old value(s) - for tracking what changed from }, new_value: { type: Sequelize.TEXT, allowNull: true, - // JSON string of the new value(s) - for tracking what changed to }, field_name: { type: Sequelize.STRING, allowNull: true, - // The name of the field that was changed (status, priority, due_date, etc.) }, metadata: { type: Sequelize.TEXT, allowNull: true, - // Additional context as JSON string (e.g., source of change: 'web', 'api', 'telegram') }, created_at: { type: Sequelize.DATE, @@ -64,31 +58,22 @@ module.exports = { }, }); - // Add indexes for better query performance - await queryInterface.addIndex('task_events', ['task_id']); - await queryInterface.addIndex('task_events', ['user_id']); - await queryInterface.addIndex('task_events', ['event_type']); - await queryInterface.addIndex('task_events', ['created_at']); - await queryInterface.addIndex('task_events', ['task_id', 'event_type']); - await queryInterface.addIndex('task_events', ['task_id', 'created_at']); + await safeAddIndex(queryInterface, 'task_events', ['task_id']); + await safeAddIndex(queryInterface, 'task_events', ['user_id']); + await safeAddIndex(queryInterface, 'task_events', ['event_type']); + await safeAddIndex(queryInterface, 'task_events', ['created_at']); + await safeAddIndex(queryInterface, 'task_events', ['task_id', 'event_type']); + await safeAddIndex(queryInterface, 'task_events', ['task_id', 'created_at']); }, async down(queryInterface, Sequelize) { - // Remove indexes first - await queryInterface.removeIndex('task_events', [ - 'task_id', - 'created_at', - ]); - await queryInterface.removeIndex('task_events', [ - 'task_id', - 'event_type', - ]); + await queryInterface.removeIndex('task_events', ['task_id', 'created_at']); + await queryInterface.removeIndex('task_events', ['task_id', 'event_type']); await queryInterface.removeIndex('task_events', ['created_at']); await queryInterface.removeIndex('task_events', ['event_type']); await queryInterface.removeIndex('task_events', ['user_id']); await queryInterface.removeIndex('task_events', ['task_id']); - // Drop the table await queryInterface.dropTable('task_events'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250622053925-add-pomodoro-enabled-to-users.js b/backend/migrations/20250622053925-add-pomodoro-enabled-to-users.js index c3cb2a0..c426d1e 100644 --- a/backend/migrations/20250622053925-add-pomodoro-enabled-to-users.js +++ b/backend/migrations/20250622053925-add-pomodoro-enabled-to-users.js @@ -1,16 +1,22 @@ 'use strict'; -/** @type {import('sequelize-cli').Migration} */ +const { safeAddColumns } = require('../utils/migration-utils'); + module.exports = { async up(queryInterface, Sequelize) { - await queryInterface.addColumn('users', 'pomodoro_enabled', { - type: Sequelize.BOOLEAN, - allowNull: false, - defaultValue: true, - }); + await safeAddColumns(queryInterface, 'users', [ + { + name: 'pomodoro_enabled', + definition: { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + }, + ]); }, async down(queryInterface, Sequelize) { await queryInterface.removeColumn('users', 'pomodoro_enabled'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250623000001-add-uuid-to-tasks.js b/backend/migrations/20250623000001-add-uuid-to-tasks.js index 2ab5bb2..15f94ea 100644 --- a/backend/migrations/20250623000001-add-uuid-to-tasks.js +++ b/backend/migrations/20250623000001-add-uuid-to-tasks.js @@ -1,16 +1,21 @@ 'use strict'; const { v4: uuidv4 } = require('uuid'); +const { safeAddColumns, safeAddIndex } = require('../utils/migration-utils'); module.exports = { async up(queryInterface, Sequelize) { - // Add UUID column to tasks table (without unique constraint initially) - await queryInterface.addColumn('tasks', 'uuid', { - type: Sequelize.UUID, - allowNull: true, - }); - // Backfill existing tasks with UUIDs + await safeAddColumns(queryInterface, 'tasks', [ + { + name: 'uuid', + definition: { + type: Sequelize.UUID, + allowNull: true, + }, + }, + ]); + const tasks = await queryInterface.sequelize.query( 'SELECT id FROM tasks WHERE uuid IS NULL', { type: Sequelize.QueryTypes.SELECT } @@ -24,18 +29,16 @@ module.exports = { ); } - // Add unique index for UUID - await queryInterface.addIndex('tasks', ['uuid'], { + await safeAddIndex(queryInterface, 'tasks', ['uuid'], { unique: true, name: 'tasks_uuid_unique', }); }, async down(queryInterface, Sequelize) { - // Remove index first + await queryInterface.removeIndex('tasks', 'tasks_uuid_unique'); - // Remove UUID column await queryInterface.removeColumn('tasks', 'uuid'); }, }; diff --git a/backend/migrations/20250623000003-create-notes-tags-table.js b/backend/migrations/20250623000003-create-notes-tags-table.js index b5716ca..5639e62 100644 --- a/backend/migrations/20250623000003-create-notes-tags-table.js +++ b/backend/migrations/20250623000003-create-notes-tags-table.js @@ -2,7 +2,7 @@ module.exports = { async up(queryInterface, Sequelize) { - // Check if notes_tags table exists + const tables = await queryInterface.showAllTables(); if (!tables.includes('notes_tags')) { await queryInterface.createTable('notes_tags', { @@ -34,7 +34,6 @@ module.exports = { }, }); - // Add unique index await queryInterface.addIndex('notes_tags', ['note_id', 'tag_id'], { unique: true, name: 'notes_tags_unique_idx', diff --git a/backend/migrations/20250623000004-add-timestamps-to-notes-tags.js b/backend/migrations/20250623000004-add-timestamps-to-notes-tags.js index 86b507e..038ee79 100644 --- a/backend/migrations/20250623000004-add-timestamps-to-notes-tags.js +++ b/backend/migrations/20250623000004-add-timestamps-to-notes-tags.js @@ -1,29 +1,31 @@ 'use strict'; +const { safeAddColumns } = require('../utils/migration-utils'); + module.exports = { async up(queryInterface, Sequelize) { - // Add created_at and updated_at columns to notes_tags table - try { - await queryInterface.addColumn('notes_tags', 'created_at', { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), - }); - - await queryInterface.addColumn('notes_tags', 'updated_at', { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), - }); - - console.log('Successfully added timestamps to notes_tags table'); - } catch (error) { - console.error('Error adding timestamps to notes_tags:', error); - } + await safeAddColumns(queryInterface, 'notes_tags', [ + { + name: 'created_at', + definition: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + }, + { + name: 'updated_at', + definition: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + }, + ]); }, async down(queryInterface, Sequelize) { await queryInterface.removeColumn('notes_tags', 'created_at'); await queryInterface.removeColumn('notes_tags', 'updated_at'); }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250623000005-add-timestamps-to-projects-tags.js b/backend/migrations/20250623000005-add-timestamps-to-projects-tags.js index f0bb039..6ab3f90 100644 --- a/backend/migrations/20250623000005-add-timestamps-to-projects-tags.js +++ b/backend/migrations/20250623000005-add-timestamps-to-projects-tags.js @@ -1,14 +1,16 @@ 'use strict'; +const { safeCreateTable, safeAddColumns } = require('../utils/migration-utils'); + module.exports = { async up(queryInterface, Sequelize) { - // Create the projects_tags table if it doesn't exist + const tableExists = await queryInterface .showAllTables() .then((tables) => tables.includes('projects_tags')); if (!tableExists) { - await queryInterface.createTable('projects_tags', { + await safeCreateTable(queryInterface, 'projects_tags', { project_id: { type: Sequelize.INTEGER, allowNull: false, @@ -41,43 +43,41 @@ module.exports = { }, }); - // Add composite primary key await queryInterface.addConstraint('projects_tags', { fields: ['project_id', 'tag_id'], type: 'primary key', name: 'projects_tags_pkey', }); } else { - // Add timestamps if table exists but doesn't have them - try { - await queryInterface.addColumn('projects_tags', 'created_at', { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), - }); - } catch (error) { - // Column might already exist - } - try { - await queryInterface.addColumn('projects_tags', 'updated_at', { - type: Sequelize.DATE, - allowNull: false, - defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), - }); - } catch (error) { - // Column might already exist - } + await safeAddColumns(queryInterface, 'projects_tags', [ + { + name: 'created_at', + definition: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + }, + { + name: 'updated_at', + definition: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + }, + }, + ]); } }, async down(queryInterface, Sequelize) { - // Remove timestamps or drop table if needed + try { await queryInterface.removeColumn('projects_tags', 'created_at'); await queryInterface.removeColumn('projects_tags', 'updated_at'); } catch (error) { - // Columns might not exist + } }, -}; +}; \ No newline at end of file diff --git a/backend/migrations/20250711000001-add-suggestion-metadata-to-inbox-items.js b/backend/migrations/20250711000001-add-suggestion-metadata-to-inbox-items.js index 9153028..f0fd6de 100644 --- a/backend/migrations/20250711000001-add-suggestion-metadata-to-inbox-items.js +++ b/backend/migrations/20250711000001-add-suggestion-metadata-to-inbox-items.js @@ -1,36 +1,51 @@ 'use strict'; +const { safeAddColumns } = require('../utils/migration-utils'); + module.exports = { async up(queryInterface, Sequelize) { - await queryInterface.addColumn('inbox_items', 'suggested_type', { - type: Sequelize.STRING, - allowNull: true, - comment: 'AI suggested item type: task, note, or null' - }); - - await queryInterface.addColumn('inbox_items', 'suggested_reason', { - type: Sequelize.STRING, - allowNull: true, - comment: 'Reason for suggestion: verb_detected, bookmark_tag, etc.' - }); - - await queryInterface.addColumn('inbox_items', 'parsed_tags', { - type: Sequelize.JSON, - allowNull: true, - comment: 'Array of parsed hashtags from content' - }); - - await queryInterface.addColumn('inbox_items', 'parsed_projects', { - type: Sequelize.JSON, - allowNull: true, - comment: 'Array of parsed project references from content' - }); - - await queryInterface.addColumn('inbox_items', 'cleaned_content', { - type: Sequelize.STRING, - allowNull: true, - comment: 'Content with tags and project references removed' - }); + await safeAddColumns(queryInterface, 'inbox_items', [ + { + name: 'suggested_type', + definition: { + type: Sequelize.STRING, + allowNull: true, + comment: 'AI suggested item type: task, note, or null' + }, + }, + { + name: 'suggested_reason', + definition: { + type: Sequelize.STRING, + allowNull: true, + comment: 'Reason for suggestion: verb_detected, bookmark_tag, etc.' + }, + }, + { + name: 'parsed_tags', + definition: { + type: Sequelize.JSON, + allowNull: true, + comment: 'Array of parsed hashtags from content' + }, + }, + { + name: 'parsed_projects', + definition: { + type: Sequelize.JSON, + allowNull: true, + comment: 'Array of parsed project references from content' + }, + }, + { + name: 'cleaned_content', + definition: { + type: Sequelize.STRING, + allowNull: true, + comment: 'Content with tags and project references removed' + }, + }, + ]); }, async down(queryInterface, Sequelize) { diff --git a/backend/migrations/20250713072131-add-productivity-assistant-columns.js b/backend/migrations/20250713072131-add-productivity-assistant-columns.js index 7ad1828..4d61ca7 100644 --- a/backend/migrations/20250713072131-add-productivity-assistant-columns.js +++ b/backend/migrations/20250713072131-add-productivity-assistant-columns.js @@ -3,10 +3,9 @@ module.exports = { async up(queryInterface, Sequelize) { try { - // Get current table schema + const tableInfo = await queryInterface.describeTable('users'); - // Define columns to add const columnsToAdd = [ { name: 'productivity_assistant_enabled', @@ -26,7 +25,6 @@ module.exports = { }, ]; - // Add only missing columns for (const column of columnsToAdd) { if (!(column.name in tableInfo)) { await queryInterface.addColumn( @@ -43,7 +41,7 @@ module.exports = { }, async down(queryInterface, Sequelize) { - // Remove the added columns + await queryInterface.removeColumn( 'users', 'productivity_assistant_enabled' diff --git a/backend/migrations/20250713233301-add-title-to-inbox-items.js b/backend/migrations/20250713233301-add-title-to-inbox-items.js new file mode 100644 index 0000000..7c9a426 --- /dev/null +++ b/backend/migrations/20250713233301-add-title-to-inbox-items.js @@ -0,0 +1,23 @@ +'use strict'; + +const { safeAddColumns } = require('../utils/migration-utils'); + +module.exports = { + async up(queryInterface, Sequelize) { + await safeAddColumns(queryInterface, 'inbox_items', [ + { + name: 'title', + definition: { + type: Sequelize.STRING, + allowNull: true, + comment: + 'Optional title field for inbox items, auto-generated for long content', + }, + }, + ]); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.removeColumn('inbox_items', 'title'); + }, +}; diff --git a/backend/package.json b/backend/package.json index bd63e5a..733f613 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,11 +1,11 @@ { "name": "backend", - "version": "v0.70.1", + "version": "v0.70.3", "description": "", "main": "index.js", "scripts": { - "start": "node app.js", - "dev": "nodemon app.js", + "start": "./cmd/start.sh", + "start:dev": "nodemon app.js", "test": "cross-env NODE_ENV=test jest", "test:watch": "cross-env NODE_ENV=test jest --watch", "test:coverage": "cross-env NODE_ENV=test jest --coverage", diff --git a/backend/utils/migration-utils.js b/backend/utils/migration-utils.js new file mode 100644 index 0000000..cb580d6 --- /dev/null +++ b/backend/utils/migration-utils.js @@ -0,0 +1,62 @@ +'use strict'; + +async function safeAddColumns(queryInterface, tableName, columns) { + try { + const tableInfo = await queryInterface.describeTable(tableName); + + for (const column of columns) { + if (!(column.name in tableInfo)) { + await queryInterface.addColumn( + tableName, + column.name, + column.definition + ); + } + } + } catch (error) { + console.log(`Migration error for table ${tableName}:`, error.message); + throw error; + } +} + +async function safeCreateTable(queryInterface, tableName, tableDefinition) { + try { + const tableExists = await queryInterface + .showAllTables() + .then((tables) => tables.includes(tableName)); + + if (!tableExists) { + await queryInterface.createTable(tableName, tableDefinition); + } + } catch (error) { + console.log( + `Migration error creating table ${tableName}:`, + error.message + ); + throw error; + } +} + +async function safeAddIndex(queryInterface, tableName, fields, options = {}) { + try { + const indexes = await queryInterface.showIndex(tableName); + const indexExists = indexes.some((index) => + index.fields.some((field) => fields.includes(field.attribute)) + ); + + if (!indexExists) { + await queryInterface.addIndex(tableName, fields, options); + } + } catch (error) { + console.log( + `Migration error adding index to ${tableName}:`, + error.message + ); + } +} + +module.exports = { + safeAddColumns, + safeCreateTable, + safeAddIndex, +}; diff --git a/package.json b/package.json index cc2d5d0..43fb561 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tududi", - "version": "v0.70.1", + "version": "v0.70.3", "description": "Self-hosted task management with hierarchical organization (Areas > Projects > Tasks), multi-language support, and Telegram integration. Built with React/TypeScript frontend and functional programming Express.js backend.", "main": "index.js", "directories": {