exports.up = async function (knex) { await knex.schema.createTable('approval_definitions', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('name', 191).notNullable(); table.text('description'); table.string('triggerType', 191).notNullable(); table.string('targetObjectType', 191); table.json('entryCriteria'); table.json('steps'); table.json('votingPolicy'); table.string('rejectionRule', 191); table.string('materialChangePolicy', 191); table.integer('version').notNullable().defaultTo(1); table.boolean('isActive').notNullable().defaultTo(true); table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()); table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()); }); await knex.schema.createTable('approval_requests', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('definitionId', 191).notNullable(); table.string('status', 191).notNullable().defaultTo('pending'); table.string('targetObjectType', 191).notNullable(); table.string('targetObjectId', 191).notNullable(); table.string('action', 191); table.string('stateFrom', 191); table.string('stateTo', 191); table.json('fieldChanges'); table.json('snapshot'); table.string('versionHash', 191); table.string('submittedById', 191); table.timestamp('submittedAt'); table.string('currentStepKey', 191); table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()); table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()); table.index(['definitionId']); table.index(['targetObjectType', 'targetObjectId']); table.index(['submittedById']); table .foreign('definitionId') .references('id') .inTable('approval_definitions') .onDelete('CASCADE') .onUpdate('CASCADE'); table .foreign('submittedById') .references('id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); }); await knex.schema.createTable('approval_steps', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('requestId', 191).notNullable(); table.string('stepKey', 191).notNullable(); table.string('name', 191).notNullable(); table.integer('stepOrder').notNullable(); table.string('status', 191).notNullable().defaultTo('pending'); table.json('routing'); table.json('voting'); table.timestamp('dueAt'); table.timestamp('completedAt'); table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()); table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()); table.index(['requestId']); table.index(['status']); table .foreign('requestId') .references('id') .inTable('approval_requests') .onDelete('CASCADE') .onUpdate('CASCADE'); }); await knex.schema.createTable('approval_assignments', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('stepId', 191).notNullable(); table.string('assigneeId', 191).notNullable(); table.string('status', 191).notNullable().defaultTo('pending'); table.text('response'); table.timestamp('respondedAt'); table.timestamp('dueAt'); table.string('reassignedFromId', 191); table.string('delegatedById', 191); table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()); table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()); table.index(['stepId']); table.index(['assigneeId']); table.index(['status']); table .foreign('stepId') .references('id') .inTable('approval_steps') .onDelete('CASCADE') .onUpdate('CASCADE'); table .foreign('assigneeId') .references('id') .inTable('users') .onDelete('RESTRICT') .onUpdate('CASCADE'); table .foreign('reassignedFromId') .references('id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); table .foreign('delegatedById') .references('id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); }); await knex.schema.createTable('approval_effect_logs', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('requestId', 191).notNullable(); table.string('effectKey', 191).notNullable(); table.string('status', 191).notNullable().defaultTo('success'); table.json('response'); table.timestamp('executedAt').notNullable().defaultTo(knex.fn.now()); table.unique(['requestId', 'effectKey']); table.index(['requestId']); table .foreign('requestId') .references('id') .inTable('approval_requests') .onDelete('CASCADE') .onUpdate('CASCADE'); }); await knex.schema.createTable('tasks', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('title', 191).notNullable(); table.text('description'); table.string('status', 191).notNullable().defaultTo('open'); table.string('priority', 191); table.timestamp('dueAt'); table.string('assignedToId', 191); table.string('relatedObjectType', 191); table.string('relatedObjectId', 191); table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()); table.timestamp('updatedAt').notNullable().defaultTo(knex.fn.now()); table.index(['assignedToId']); table.index(['relatedObjectType', 'relatedObjectId']); table .foreign('assignedToId') .references('id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); }); await knex.schema.createTable('activity_logs', (table) => { table.string('id', 191).primary().defaultTo(knex.raw('(UUID())')); table.string('action', 191).notNullable(); table.string('subjectType', 191).notNullable(); table.string('subjectId', 191).notNullable(); table.text('description'); table.json('properties'); table.string('causerId', 191); table.timestamp('createdAt').notNullable().defaultTo(knex.fn.now()); table.index(['subjectType', 'subjectId']); table.index(['causerId']); table .foreign('causerId') .references('id') .inTable('users') .onDelete('SET NULL') .onUpdate('CASCADE'); }); }; exports.down = async function (knex) { await knex.schema.dropTableIfExists('activity_logs'); await knex.schema.dropTableIfExists('tasks'); await knex.schema.dropTableIfExists('approval_effect_logs'); await knex.schema.dropTableIfExists('approval_assignments'); await knex.schema.dropTableIfExists('approval_steps'); await knex.schema.dropTableIfExists('approval_requests'); await knex.schema.dropTableIfExists('approval_definitions'); };