220 lines
11 KiB
TypeScript
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`);
|
|
}
|
|
}
|