102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
|
|
import type {
|
||
|
|
EntityReference,
|
||
|
|
EntitySummaryResolver,
|
||
|
|
RelationshipContext,
|
||
|
|
RelationshipReadAccess,
|
||
|
|
SummaryResolution,
|
||
|
|
} from "@metis/relationship-contracts";
|
||
|
|
import {
|
||
|
|
EntityReferenceSchema,
|
||
|
|
EntitySummarySchema,
|
||
|
|
RelationshipContextSchema,
|
||
|
|
RelationshipReadError,
|
||
|
|
} from "@metis/relationship-contracts";
|
||
|
|
import { Result } from "@repo/rdx-utils";
|
||
|
|
import type { Sequelize } from "sequelize";
|
||
|
|
import { QueryTypes } from "sequelize";
|
||
|
|
import { z } from "zod/v4";
|
||
|
|
|
||
|
|
const RowSchema = z.object({
|
||
|
|
id: z.uuid(),
|
||
|
|
name: z.string().min(1),
|
||
|
|
active: z.union([z.boolean(), z.literal(0), z.literal(1)]).transform(Boolean),
|
||
|
|
normalizedAddress: z.string().min(1),
|
||
|
|
});
|
||
|
|
|
||
|
|
/** Una proyección por lote, sin hidratar inventarios ni cargar sus historiales. */
|
||
|
|
export class NetworkShareSummaryResolver implements EntitySummaryResolver {
|
||
|
|
readonly providerId = "resources";
|
||
|
|
readonly entityTypes = Object.freeze(["resources:resource"] as const);
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
private readonly database: Sequelize,
|
||
|
|
private readonly access: RelationshipReadAccess
|
||
|
|
) {}
|
||
|
|
|
||
|
|
async resolveMany(
|
||
|
|
references: readonly EntityReference[],
|
||
|
|
context: RelationshipContext
|
||
|
|
): Promise<Result<readonly SummaryResolution[], RelationshipReadError>> {
|
||
|
|
if (!RelationshipContextSchema.safeParse(context).success) {
|
||
|
|
return Result.fail(new RelationshipReadError("FORBIDDEN"));
|
||
|
|
}
|
||
|
|
const authorized = await this.access.authorize(context);
|
||
|
|
|
||
|
|
if (!authorized.isSuccess) {
|
||
|
|
return authorized;
|
||
|
|
}
|
||
|
|
const parsed = z.array(EntityReferenceSchema).max(100).safeParse(references);
|
||
|
|
|
||
|
|
if (!parsed.success) {
|
||
|
|
return Result.fail(new RelationshipReadError("INVALID_QUERY"));
|
||
|
|
}
|
||
|
|
const accepted = parsed.data.filter(
|
||
|
|
(reference) =>
|
||
|
|
reference.companyId === context.companyId &&
|
||
|
|
reference.providerId === this.providerId &&
|
||
|
|
reference.entityType === "resources:resource"
|
||
|
|
);
|
||
|
|
const ids = [...new Set(accepted.map((reference) => reference.id))];
|
||
|
|
const rows =
|
||
|
|
ids.length === 0
|
||
|
|
? []
|
||
|
|
: z
|
||
|
|
.array(RowSchema)
|
||
|
|
.parse(
|
||
|
|
await this.database.query(
|
||
|
|
"SELECT r.id,r.name,r.active,d.normalizedAddress FROM resources r JOIN resource_network_shares d ON d.resourceId=r.id AND d.companyId=r.companyId WHERE r.companyId=:companyId AND r.id IN (:ids)",
|
||
|
|
{ type: QueryTypes.SELECT, replacements: { companyId: context.companyId, ids } }
|
||
|
|
)
|
||
|
|
);
|
||
|
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
||
|
|
|
||
|
|
return Result.ok(
|
||
|
|
Object.freeze(
|
||
|
|
parsed.data.map((reference): SummaryResolution => {
|
||
|
|
const row =
|
||
|
|
reference.companyId === context.companyId &&
|
||
|
|
reference.providerId === this.providerId &&
|
||
|
|
reference.entityType === "resources:resource"
|
||
|
|
? byId.get(reference.id)
|
||
|
|
: undefined;
|
||
|
|
|
||
|
|
return row
|
||
|
|
? Object.freeze({
|
||
|
|
status: "resolved",
|
||
|
|
summary: EntitySummarySchema.parse({
|
||
|
|
reference,
|
||
|
|
name: row.name,
|
||
|
|
subtitle: row.normalizedAddress,
|
||
|
|
state: {
|
||
|
|
code: row.active ? "active" : "inactive",
|
||
|
|
labelKey: row.active ? "resources:active" : "resources:inactive",
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
: Object.freeze({ status: "unavailable", reference });
|
||
|
|
})
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|