195 lines
6.9 KiB
TypeScript
195 lines
6.9 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { DataSource } from 'typeorm';
|
|
import {
|
|
AssetVersionChangeType,
|
|
AuditSource,
|
|
} from '../database/entities';
|
|
import type {
|
|
ListTemporalAssetsQueryDto,
|
|
TemporalAtQueryDto,
|
|
} from './dto/list-temporal-assets-query.dto';
|
|
|
|
export interface TemporalAssetSummary {
|
|
id: string;
|
|
assetId: string;
|
|
assetCode: string;
|
|
assetName: string;
|
|
typeId: string;
|
|
typeName: string;
|
|
informationStatus: string;
|
|
operationalStatus: string;
|
|
versionNumber: number;
|
|
changeType: AssetVersionChangeType;
|
|
changedFields: string[];
|
|
occurredAt: Date;
|
|
effectiveUntil: Date | null;
|
|
actorUserId: string | null;
|
|
actorUsername: string | null;
|
|
source: AuditSource;
|
|
requestId: string | null;
|
|
isCurrent: boolean;
|
|
}
|
|
|
|
export interface TemporalAssetDetail extends TemporalAssetSummary {
|
|
snapshot: Record<string, unknown>;
|
|
asOf: Date;
|
|
}
|
|
|
|
function temporalAssetNotFound(): NotFoundException {
|
|
return new NotFoundException({
|
|
code: 'ASSET_NOT_EXISTING_AT_DATE',
|
|
message: 'El activo no tenía una versión registrada en la fecha solicitada',
|
|
});
|
|
}
|
|
|
|
@Injectable()
|
|
export class AssetTemporalService {
|
|
constructor(private readonly dataSource: DataSource) {}
|
|
|
|
async list(query: ListTemporalAssetsQueryDto) {
|
|
const parameters: unknown[] = [new Date(query.at)];
|
|
const conditions: string[] = [];
|
|
const add = (value: unknown): string => {
|
|
parameters.push(value);
|
|
return `$${parameters.length}`;
|
|
};
|
|
|
|
if (query.search?.trim()) {
|
|
const search = add(`%${query.search.trim()}%`);
|
|
conditions.push(`(
|
|
version.snapshot->>'code' ILIKE ${search}
|
|
OR version.snapshot->>'name' ILIKE ${search}
|
|
)`);
|
|
}
|
|
if (query.typeId) {
|
|
conditions.push(`version.snapshot #>> '{type,id}' = ${add(query.typeId)}`);
|
|
}
|
|
if (query.status) {
|
|
conditions.push(`version.snapshot->>'informationStatus' = ${add(query.status)}`);
|
|
}
|
|
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
const selected = this.selectedAt('$1');
|
|
const [countRow] = (await this.dataSource.query(
|
|
`WITH selected_version AS (${selected})
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM selected_version version
|
|
${where}`,
|
|
parameters,
|
|
)) as Array<{ total: number }>;
|
|
const total = Number(countRow?.total ?? 0);
|
|
const paginated = [
|
|
...parameters,
|
|
query.pageSize,
|
|
(query.page - 1) * query.pageSize,
|
|
];
|
|
const limit = `$${parameters.length + 1}`;
|
|
const offset = `$${parameters.length + 2}`;
|
|
const data = (await this.dataSource.query(
|
|
`WITH selected_version AS (${selected})
|
|
${this.selectSummary()}
|
|
FROM selected_version version
|
|
INNER JOIN assets current_asset ON current_asset.id = version.asset_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT candidate.occurred_at
|
|
FROM asset_versions candidate
|
|
WHERE candidate.asset_id = version.asset_id
|
|
AND candidate.version_number > version.version_number
|
|
ORDER BY candidate.version_number ASC
|
|
LIMIT 1
|
|
) next_version ON true
|
|
${where}
|
|
ORDER BY "assetName" ASC, "assetCode" ASC
|
|
LIMIT ${limit} OFFSET ${offset}`,
|
|
paginated,
|
|
)) as TemporalAssetSummary[];
|
|
|
|
return {
|
|
data,
|
|
asOf: new Date(query.at),
|
|
meta: {
|
|
page: query.page,
|
|
pageSize: query.pageSize,
|
|
total,
|
|
totalPages: total === 0 ? 0 : Math.ceil(total / query.pageSize),
|
|
},
|
|
};
|
|
}
|
|
|
|
async get(assetId: string, query: TemporalAtQueryDto): Promise<TemporalAssetDetail> {
|
|
const [row] = (await this.dataSource.query(
|
|
`${this.selectSummary()}, version.snapshot
|
|
FROM asset_versions version
|
|
INNER JOIN assets current_asset ON current_asset.id = version.asset_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT candidate.occurred_at
|
|
FROM asset_versions candidate
|
|
WHERE candidate.asset_id = version.asset_id
|
|
AND candidate.version_number > version.version_number
|
|
ORDER BY candidate.version_number ASC
|
|
LIMIT 1
|
|
) next_version ON true
|
|
WHERE version.asset_id = $1
|
|
AND version.occurred_at <= $2
|
|
ORDER BY version.occurred_at DESC, version.version_number DESC
|
|
LIMIT 1`,
|
|
[assetId, new Date(query.at)],
|
|
)) as TemporalAssetDetail[];
|
|
if (!row) throw temporalAssetNotFound();
|
|
const asOf = new Date(query.at);
|
|
row.asOf = asOf;
|
|
const [context] = (await this.dataSource.query(`
|
|
SELECT
|
|
CASE WHEN parent.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',parent.id,'code',parent.code,'name',parent.name) END AS parent,
|
|
CASE WHEN area.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',area.id,'code',area.code,'name',area.name) END AS "operationalArea",
|
|
CASE WHEN company.id IS NULL THEN NULL ELSE JSONB_BUILD_OBJECT('id',company.id,'code',company.code,'name',company.name) END AS "operatorCompany"
|
|
FROM asset_context_history history
|
|
LEFT JOIN assets parent ON parent.id=history.parent_id
|
|
LEFT JOIN assets area ON area.id=history.operational_area_id
|
|
LEFT JOIN assets company ON company.id=history.operator_company_id
|
|
WHERE history.asset_id=$1
|
|
AND history.valid_from <= $2
|
|
AND (history.valid_until IS NULL OR history.valid_until > $2)
|
|
ORDER BY history.valid_from DESC
|
|
LIMIT 1
|
|
`, [assetId, asOf])) as Array<{ parent: Record<string, unknown> | null; operationalArea: Record<string, unknown> | null; operatorCompany: Record<string, unknown> | null }>;
|
|
if (context && row.snapshot) {
|
|
row.snapshot = {
|
|
...row.snapshot,
|
|
parent: context.parent,
|
|
operationalArea: context.operationalArea,
|
|
operatorCompany: context.operatorCompany,
|
|
};
|
|
}
|
|
return row;
|
|
}
|
|
|
|
private selectedAt(atParameter: string): string {
|
|
return `SELECT DISTINCT ON (candidate.asset_id) candidate.*
|
|
FROM asset_versions candidate
|
|
WHERE candidate.occurred_at <= ${atParameter}
|
|
ORDER BY candidate.asset_id, candidate.occurred_at DESC, candidate.version_number DESC`;
|
|
}
|
|
|
|
private selectSummary(): string {
|
|
return `SELECT
|
|
version.id,
|
|
version.asset_id AS "assetId",
|
|
version.snapshot->>'code' AS "assetCode",
|
|
version.snapshot->>'name' AS "assetName",
|
|
version.snapshot #>> '{type,id}' AS "typeId",
|
|
version.snapshot #>> '{type,name}' AS "typeName",
|
|
version.snapshot->>'informationStatus' AS "informationStatus",
|
|
version.snapshot->>'operationalStatus' AS "operationalStatus",
|
|
version.version_number AS "versionNumber",
|
|
version.change_type AS "changeType",
|
|
version.changed_fields AS "changedFields",
|
|
version.occurred_at AS "occurredAt",
|
|
next_version.occurred_at AS "effectiveUntil",
|
|
version.actor_user_id AS "actorUserId",
|
|
version.actor_username AS "actorUsername",
|
|
version.source,
|
|
version.request_id AS "requestId",
|
|
(version.version_number = current_asset.current_version) AS "isCurrent"`;
|
|
}
|
|
}
|