Files
dh-inspeccion-v2/api-v3/src/database/migrations/1789063200000-phase-d5-5-web-planning-checklist.ts
T

220 lines
11 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD55WebPlanningChecklist1789063200000 implements MigrationInterface {
name = 'PhaseD55WebPlanningChecklist1789063200000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE inspection_visits
ADD COLUMN operational_area_id uuid,
ADD COLUMN operator_company_id uuid,
ADD COLUMN checklist_generation integer NOT NULL DEFAULT 0,
ADD COLUMN checklist_generated_at timestamptz,
ADD CONSTRAINT fk_inspection_visits_operational_area FOREIGN KEY (operational_area_id)
REFERENCES assets(id) ON DELETE RESTRICT,
ADD CONSTRAINT fk_inspection_visits_operator_company FOREIGN KEY (operator_company_id)
REFERENCES assets(id) ON DELETE RESTRICT,
ADD CONSTRAINT chk_inspection_visits_checklist_generation CHECK (checklist_generation >= 0),
ADD CONSTRAINT chk_inspection_visits_operational_context_pair CHECK (
(operational_area_id IS NULL AND operator_company_id IS NULL)
OR (operational_area_id IS NOT NULL AND operator_company_id IS NOT NULL)
)
`);
await queryRunner.query(`CREATE INDEX idx_inspection_visits_operational_context ON inspection_visits (operational_area_id, operator_company_id)`);
await queryRunner.query(`
WITH context AS (
SELECT
visit.id,
COALESCE(
(MAX(scope.id::text) FILTER (WHERE scope_type.operational_role = 'AREA'))::uuid,
MIN(asset.operational_area_id::text)::uuid
) AS area_id,
CASE
WHEN COUNT(DISTINCT asset.operator_company_id) FILTER (WHERE asset.operator_company_id IS NOT NULL) = 1
THEN MIN(asset.operator_company_id::text)::uuid
ELSE NULL
END AS company_id
FROM inspection_visits visit
LEFT JOIN assets scope ON scope.id = visit.scope_asset_id
LEFT JOIN asset_types scope_type ON scope_type.id = scope.asset_type_id
LEFT JOIN inspection_visit_assets link ON link.visit_id = visit.id AND link.included = true
LEFT JOIN assets asset ON asset.id = link.asset_id
GROUP BY visit.id
)
UPDATE inspection_visits visit
SET operational_area_id = context.area_id,
operator_company_id = context.company_id
FROM context
WHERE context.id = visit.id
AND context.area_id IS NOT NULL
AND context.company_id IS NOT NULL
`);
await queryRunner.query(`
ALTER TABLE inspection_visit_assets
ADD COLUMN planning_source varchar(24) NOT NULL DEFAULT 'LEGACY',
ADD COLUMN exclusion_reason text,
ADD COLUMN excluded_by uuid,
ADD COLUMN excluded_at timestamptz,
ADD CONSTRAINT fk_inspection_visit_assets_excluded_by FOREIGN KEY (excluded_by)
REFERENCES users(id) ON DELETE SET NULL,
ADD CONSTRAINT chk_inspection_visit_assets_planning_source CHECK (
planning_source IN ('LEGACY','AUTOMATIC','PREVENTIVE','VERIFICATION')
)
`);
await queryRunner.query(`
UPDATE inspection_visit_assets link
SET planning_source = 'VERIFICATION'
WHERE EXISTS (
SELECT 1
FROM inspection_finding_verification_visits verification_link
INNER JOIN inspection_findings finding ON finding.id = verification_link.finding_id
WHERE verification_link.visit_id = link.visit_id
AND finding.asset_id = link.asset_id
)
`);
await queryRunner.query(`
UPDATE inspection_visit_assets
SET exclusion_reason = 'Exclusión histórica previa a D5.5',
excluded_by = added_by,
excluded_at = updated_at
WHERE included = false
AND exclusion_reason IS NULL
`);
await queryRunner.query(`
ALTER TABLE inspection_visit_assets
ADD CONSTRAINT chk_inspection_visit_assets_exclusion_state CHECK (
(included = true AND exclusion_reason IS NULL AND excluded_at IS NULL)
OR (included = false AND LENGTH(TRIM(COALESCE(exclusion_reason, ''))) >= 10 AND excluded_at IS NOT NULL)
)
`);
await queryRunner.query(`
CREATE TABLE inspection_visit_checklist_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
visit_id uuid NOT NULL,
generation_number integer NOT NULL,
finding_id uuid NOT NULL,
asset_id uuid NOT NULL,
item_kind varchar(32) NOT NULL,
reference_on date,
finding_status varchar(20) NOT NULL,
finding_code varchar(40) NOT NULL,
finding_title varchar(500) NOT NULL,
severity smallint,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_inspection_visit_checklist_visit FOREIGN KEY (visit_id)
REFERENCES inspection_visits(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_visit_checklist_finding FOREIGN KEY (finding_id)
REFERENCES inspection_findings(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_visit_checklist_asset FOREIGN KEY (asset_id)
REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT uq_inspection_visit_checklist_generation_finding UNIQUE (visit_id, generation_number, finding_id),
CONSTRAINT chk_inspection_visit_checklist_generation CHECK (generation_number > 0),
CONSTRAINT chk_inspection_visit_checklist_kind CHECK (
item_kind IN ('ANTECEDENT','COMPANY_OVERDUE','VERIFICATION_OVERDUE','UPCOMING_CONTROL')
),
CONSTRAINT chk_inspection_visit_checklist_severity CHECK (severity IS NULL OR severity BETWEEN 1 AND 10)
)
`);
await queryRunner.query(`CREATE INDEX idx_inspection_visit_checklist_current ON inspection_visit_checklist_items (visit_id, generation_number, item_kind)`);
await queryRunner.query(`CREATE INDEX idx_inspection_visit_checklist_asset ON inspection_visit_checklist_items (visit_id, generation_number, asset_id)`);
await queryRunner.query(`
CREATE TABLE inspection_visit_asset_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
visit_id uuid NOT NULL,
asset_id uuid NOT NULL,
event_type varchar(32) NOT NULL,
reason text,
actor_user_id uuid,
occurred_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
metadata jsonb,
CONSTRAINT fk_inspection_visit_asset_events_visit FOREIGN KEY (visit_id)
REFERENCES inspection_visits(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_visit_asset_events_asset FOREIGN KEY (asset_id)
REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_visit_asset_events_actor FOREIGN KEY (actor_user_id)
REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT chk_inspection_visit_asset_events_type CHECK (
event_type IN ('LEGACY_INCLUDED','AUTO_INCLUDED','PREVENTIVE_INCLUDED','VERIFICATION_INCLUDED','EXCLUDED','REINCLUDED')
),
CONSTRAINT chk_inspection_visit_asset_events_exclusion_reason CHECK (
event_type <> 'EXCLUDED' OR LENGTH(TRIM(COALESCE(reason, ''))) >= 10
)
)
`);
await queryRunner.query(`CREATE INDEX idx_inspection_visit_asset_events_visit ON inspection_visit_asset_events (visit_id, occurred_at, id)`);
await queryRunner.query(`CREATE INDEX idx_inspection_visit_asset_events_asset ON inspection_visit_asset_events (asset_id, occurred_at, id)`);
await queryRunner.query(`
INSERT INTO inspection_visit_asset_events (
visit_id, asset_id, event_type, actor_user_id, occurred_at, metadata
)
SELECT
link.visit_id,
link.asset_id,
CASE WHEN link.planning_source = 'VERIFICATION' THEN 'VERIFICATION_INCLUDED' ELSE 'LEGACY_INCLUDED' END,
link.added_by,
link.created_at,
JSONB_BUILD_OBJECT('backfill', true, 'planningSource', link.planning_source)
FROM inspection_visit_assets link
`);
await queryRunner.query(`
INSERT INTO inspection_visit_asset_events (
visit_id, asset_id, event_type, reason, actor_user_id, occurred_at, metadata
)
SELECT
link.visit_id,
link.asset_id,
'EXCLUDED',
link.exclusion_reason,
link.excluded_by,
COALESCE(link.excluded_at, link.updated_at),
JSONB_BUILD_OBJECT('backfill', true)
FROM inspection_visit_assets link
WHERE link.included = false
`);
const appRole = process.env.DB_APP_USER;
if (!appRole) throw new Error('Missing required environment variable: DB_APP_USER');
const applicationRole = quoteIdentifier(appRole);
await queryRunner.query(`
GRANT SELECT, INSERT ON TABLE
inspection_visit_checklist_items, inspection_visit_asset_events
TO ${applicationRole}
`);
await queryRunner.query(`
REVOKE UPDATE, DELETE ON TABLE
inspection_visit_checklist_items, inspection_visit_asset_events
FROM ${applicationRole}
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE inspection_visit_asset_events`);
await queryRunner.query(`DROP TABLE inspection_visit_checklist_items`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP CONSTRAINT chk_inspection_visit_assets_exclusion_state`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP CONSTRAINT fk_inspection_visit_assets_excluded_by`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP CONSTRAINT chk_inspection_visit_assets_planning_source`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP COLUMN excluded_at`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP COLUMN excluded_by`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP COLUMN exclusion_reason`);
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP COLUMN planning_source`);
await queryRunner.query(`DROP INDEX idx_inspection_visits_operational_context`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP CONSTRAINT chk_inspection_visits_operational_context_pair`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP CONSTRAINT chk_inspection_visits_checklist_generation`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP CONSTRAINT fk_inspection_visits_operator_company`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP CONSTRAINT fk_inspection_visits_operational_area`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP COLUMN checklist_generated_at`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP COLUMN checklist_generation`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP COLUMN operator_company_id`);
await queryRunner.query(`ALTER TABLE inspection_visits DROP COLUMN operational_area_id`);
}
}