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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
135 lines
5.2 KiB
JavaScript
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 };
|