PSA/server/migrations/20260525231145_create_ticket_audit_logs.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

135 lines
5.2 KiB
JavaScript

/**
* Create ticket_audit_logs table
*
* Operational, user-facing ticket activity timeline (not a compliance-grade
* immutable ledger). Captures comments, internal notes, customer replies,
* curated field changes, lifecycle transitions, inbound-email source actions,
* and document activity in one tenant/ticket-scoped stream.
*
* See ee/docs/plans/2026-05-25-ticket-audit-logs/PRD.md for design intent.
*/
// Helper: distribute a table by tenant if Citus is available
async function distributeIfCitus(knex, tableName) {
const citusFn = await knex.raw(`
SELECT EXISTS (
SELECT 1 FROM pg_proc WHERE proname = 'create_distributed_table'
) AS exists;
`);
if (citusFn.rows?.[0]?.exists) {
const alreadyDistributed = await knex.raw(`
SELECT EXISTS (
SELECT 1 FROM pg_dist_partition
WHERE logicalrelid = '${tableName}'::regclass
) AS is_distributed;
`);
if (!alreadyDistributed.rows?.[0]?.is_distributed) {
await knex.raw(`SELECT create_distributed_table('${tableName}', 'tenant')`);
}
}
}
exports.up = async function (knex) {
console.log('Creating ticket_audit_logs table...');
if (!(await knex.schema.hasTable('ticket_audit_logs'))) {
await knex.schema.createTable('ticket_audit_logs', (table) => {
table.uuid('tenant').notNullable();
table.uuid('audit_id').defaultTo(knex.raw('gen_random_uuid()')).notNullable();
table.uuid('ticket_id').notNullable()
.comment('The ticket this activity entry belongs to.');
table.string('event_type', 64).notNullable()
.comment('Event name aligned with ticket domain events where practical (e.g., TICKET_CREATED, TICKET_STATUS_CHANGED, TICKET_COMMENT_ADDED).');
table.string('entity_type', 32).notNullable()
.comment('Type of related entity: ticket, comment, document, email, system.');
table.string('entity_id', 128).nullable()
.comment('Optional identifier of the related entity (comment_id, document_id, etc.).');
table.string('actor_type', 32).notNullable()
.comment('Actor classification: user, contact, system, api, email_sender, workflow.');
table.uuid('actor_user_id').nullable()
.comment('User ID of the actor when actor_type=user.');
table.uuid('actor_contact_id').nullable()
.comment('Contact ID of the actor when actor_type=contact or email_sender.');
table.string('actor_display_name', 256).nullable()
.comment('Best-effort cached display name for the actor (not required for correctness).');
table.string('source', 32).notNullable()
.comment('Origin: ui, api, client_portal, inbound_email, workflow, system.');
table.timestamp('occurred_at', { useTz: true }).notNullable()
.comment('When the event actually occurred (may differ from created_at if backdated by source).');
table.jsonb('changes').notNullable().defaultTo('{}')
.comment('Structured old/new diffs for field changes. Curated to user-meaningful fields only.');
table.jsonb('details').notNullable().defaultTo('{}')
.comment('Free-form event metadata (e.g., inbound email message_id, comment preview, document name). Must not contain raw email bodies or full old/new comment bodies.');
table.timestamp('created_at', { useTz: true })
.defaultTo(knex.fn.now())
.notNullable();
table.primary(['tenant', 'audit_id']);
table.foreign('tenant').references('tenant').inTable('tenants');
// Primary lookup pattern: list activity for a ticket newest-first.
table.index(['tenant', 'ticket_id', 'occurred_at', 'audit_id'], 'ticket_audit_logs_ticket_time_idx');
});
}
await distributeIfCitus(knex, 'ticket_audit_logs');
// Composite FK to tickets — same pattern as sla_audit_log. Allow
// detach-on-delete behavior to preserve history if the ticket is removed.
await knex.raw(`
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'ticket_audit_logs_ticket_fkey'
) THEN
ALTER TABLE ticket_audit_logs
ADD CONSTRAINT ticket_audit_logs_ticket_fkey
FOREIGN KEY (tenant, ticket_id)
REFERENCES tickets(tenant, ticket_id);
END IF;
END $$;
`);
// Optional FK on actor_user_id (nullable) — MATCH SIMPLE skips when null.
await knex.raw(`
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'ticket_audit_logs_actor_user_fkey'
) THEN
ALTER TABLE ticket_audit_logs
ADD CONSTRAINT ticket_audit_logs_actor_user_fkey
FOREIGN KEY (tenant, actor_user_id)
REFERENCES users(tenant, user_id);
END IF;
END $$;
`);
await knex.raw(`
COMMENT ON TABLE ticket_audit_logs IS 'Operational ticket activity timeline (v1, not compliance-grade). See ee/docs/plans/2026-05-25-ticket-audit-logs/PRD.md.';
`);
console.log('ticket_audit_logs table created');
};
exports.down = async function (knex) {
console.log('Dropping ticket_audit_logs table...');
await knex.schema.dropTableIfExists('ticket_audit_logs');
console.log('ticket_audit_logs table dropped');
};
// Citus requires FK manipulation to run outside a transaction block.
exports.config = { transaction: false };