198 lines
8.2 KiB
TypeScript
198 lines
8.2 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
function quoteIdentifier(identifier: string): string {
|
|
return `"${identifier.replaceAll('"', '""')}"`;
|
|
}
|
|
|
|
export class PhaseF21FieldInventory1789668000000 implements MigrationInterface {
|
|
name = 'PhaseF21FieldInventory1789668000000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE field_inventory_code_sequences (
|
|
year integer PRIMARY KEY,
|
|
last_value integer NOT NULL DEFAULT 0,
|
|
CONSTRAINT chk_field_inventory_code_sequences_year CHECK (year BETWEEN 2000 AND 9999),
|
|
CONSTRAINT chk_field_inventory_code_sequences_value CHECK (last_value >= 0)
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE TABLE asset_field_capture_events (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
visit_id uuid NOT NULL,
|
|
asset_id uuid NOT NULL,
|
|
media_id uuid,
|
|
event_type varchar(24) NOT NULL,
|
|
device_latitude numeric(9, 6) NOT NULL,
|
|
device_longitude numeric(9, 6) NOT NULL,
|
|
device_accuracy_m numeric(12, 3),
|
|
device_captured_at timestamptz NOT NULL,
|
|
device_label varchar(255),
|
|
exif_latitude numeric(9, 6),
|
|
exif_longitude numeric(9, 6),
|
|
exif_captured_at timestamptz,
|
|
created_by uuid NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_asset_field_capture_visit FOREIGN KEY (visit_id)
|
|
REFERENCES inspection_visits(id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_asset_field_capture_asset FOREIGN KEY (asset_id)
|
|
REFERENCES assets(id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_asset_field_capture_media FOREIGN KEY (media_id)
|
|
REFERENCES asset_media(id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_asset_field_capture_created_by FOREIGN KEY (created_by)
|
|
REFERENCES users(id) ON DELETE RESTRICT,
|
|
CONSTRAINT chk_asset_field_capture_event_type CHECK (event_type IN ('CREATED','PHOTO')),
|
|
CONSTRAINT chk_asset_field_capture_device_coordinates CHECK (
|
|
device_latitude BETWEEN -90 AND 90
|
|
AND device_longitude BETWEEN -180 AND 180
|
|
),
|
|
CONSTRAINT chk_asset_field_capture_accuracy CHECK (
|
|
device_accuracy_m IS NULL OR device_accuracy_m BETWEEN 0 AND 100000
|
|
),
|
|
CONSTRAINT chk_asset_field_capture_exif_coordinates CHECK (
|
|
(exif_latitude IS NULL AND exif_longitude IS NULL)
|
|
OR (
|
|
exif_latitude IS NOT NULL
|
|
AND exif_longitude IS NOT NULL
|
|
AND exif_latitude BETWEEN -90 AND 90
|
|
AND exif_longitude BETWEEN -180 AND 180
|
|
)
|
|
),
|
|
CONSTRAINT chk_asset_field_capture_media_event CHECK (
|
|
(event_type = 'CREATED' AND media_id IS NULL)
|
|
OR (event_type = 'PHOTO' AND media_id IS NOT NULL)
|
|
)
|
|
)
|
|
`);
|
|
await queryRunner.query(`CREATE INDEX idx_asset_field_capture_visit ON asset_field_capture_events (visit_id, created_at, id)`);
|
|
await queryRunner.query(`CREATE INDEX idx_asset_field_capture_asset ON asset_field_capture_events (asset_id, created_at, id)`);
|
|
await queryRunner.query(`CREATE UNIQUE INDEX uq_asset_field_capture_media ON asset_field_capture_events (media_id) WHERE media_id IS NOT NULL`);
|
|
|
|
await queryRunner.query(`
|
|
ALTER TABLE inspection_visit_assets
|
|
DROP CONSTRAINT chk_inspection_visit_assets_planning_source
|
|
`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE inspection_visit_assets
|
|
ADD CONSTRAINT chk_inspection_visit_assets_planning_source CHECK (
|
|
planning_source IN ('LEGACY','AUTOMATIC','PREVENTIVE','VERIFICATION','FIELD')
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
ALTER TABLE inspection_visit_asset_events
|
|
DROP CONSTRAINT chk_inspection_visit_asset_events_type
|
|
`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE inspection_visit_asset_events
|
|
ADD CONSTRAINT chk_inspection_visit_asset_events_type CHECK (
|
|
event_type IN ('LEGACY_INCLUDED','AUTO_INCLUDED','PREVENTIVE_INCLUDED','VERIFICATION_INCLUDED','FIELD_INCLUDED','EXCLUDED','REINCLUDED')
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
CREATE OR REPLACE FUNCTION enforce_field_inventory_capture_before_finding()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
finding_visit_id uuid;
|
|
BEGIN
|
|
SELECT act.visit_id INTO finding_visit_id
|
|
FROM inspection_acts act
|
|
WHERE act.id = NEW.act_id;
|
|
|
|
IF finding_visit_id IS NULL THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM asset_field_discoveries discovery
|
|
WHERE discovery.visit_id = finding_visit_id
|
|
AND discovery.asset_id = NEW.asset_id
|
|
) THEN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM asset_field_capture_events capture
|
|
WHERE capture.visit_id = finding_visit_id
|
|
AND capture.asset_id = NEW.asset_id
|
|
AND capture.event_type = 'CREATED'
|
|
) THEN
|
|
RAISE EXCEPTION USING
|
|
ERRCODE = '23514',
|
|
MESSAGE = 'FIELD_INVENTORY_GPS_REQUIRED';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM asset_field_capture_events capture
|
|
WHERE capture.visit_id = finding_visit_id
|
|
AND capture.asset_id = NEW.asset_id
|
|
AND capture.event_type = 'PHOTO'
|
|
) THEN
|
|
RAISE EXCEPTION USING
|
|
ERRCODE = '23514',
|
|
MESSAGE = 'FIELD_INVENTORY_PHOTO_REQUIRED';
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END
|
|
$$
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE TRIGGER trg_inspection_findings_field_capture
|
|
BEFORE INSERT OR UPDATE OF act_id, asset_id ON inspection_findings
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION enforce_field_inventory_capture_before_finding()
|
|
`);
|
|
|
|
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, UPDATE ON TABLE field_inventory_code_sequences TO ${applicationRole}`);
|
|
await queryRunner.query(`GRANT SELECT, INSERT ON TABLE asset_field_capture_events TO ${applicationRole}`);
|
|
await queryRunner.query(`REVOKE UPDATE, DELETE ON TABLE asset_field_capture_events FROM ${applicationRole}`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM asset_field_capture_events LIMIT 1) THEN
|
|
RAISE EXCEPTION 'F2.1 no puede revertirse: existen capturas GPS/EXIF de campo';
|
|
END IF;
|
|
IF EXISTS (SELECT 1 FROM inspection_visit_assets WHERE planning_source = 'FIELD' LIMIT 1) THEN
|
|
RAISE EXCEPTION 'F2.1 no puede revertirse: existen registros seleccionados o creados en campo';
|
|
END IF;
|
|
IF EXISTS (SELECT 1 FROM inspection_visit_asset_events WHERE event_type = 'FIELD_INCLUDED' LIMIT 1) THEN
|
|
RAISE EXCEPTION 'F2.1 no puede revertirse: existen eventos de Inventario de campo';
|
|
END IF;
|
|
END
|
|
$$
|
|
`);
|
|
|
|
await queryRunner.query(`DROP TRIGGER IF EXISTS trg_inspection_findings_field_capture ON inspection_findings`);
|
|
await queryRunner.query(`DROP FUNCTION IF EXISTS enforce_field_inventory_capture_before_finding()`);
|
|
|
|
await queryRunner.query(`ALTER TABLE inspection_visit_asset_events DROP CONSTRAINT chk_inspection_visit_asset_events_type`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE inspection_visit_asset_events
|
|
ADD CONSTRAINT chk_inspection_visit_asset_events_type CHECK (
|
|
event_type IN ('LEGACY_INCLUDED','AUTO_INCLUDED','PREVENTIVE_INCLUDED','VERIFICATION_INCLUDED','EXCLUDED','REINCLUDED')
|
|
)
|
|
`);
|
|
await queryRunner.query(`ALTER TABLE inspection_visit_assets DROP CONSTRAINT chk_inspection_visit_assets_planning_source`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE inspection_visit_assets
|
|
ADD CONSTRAINT chk_inspection_visit_assets_planning_source CHECK (
|
|
planning_source IN ('LEGACY','AUTOMATIC','PREVENTIVE','VERIFICATION')
|
|
)
|
|
`);
|
|
await queryRunner.query(`DROP TABLE asset_field_capture_events`);
|
|
await queryRunner.query(`DROP TABLE field_inventory_code_sequences`);
|
|
}
|
|
}
|