chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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"`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user