import type { UniqueID } from "@repo/rdx-ddd"; import { Result, normalizeSearchText } from "@repo/rdx-utils"; import type { Sequelize } from "sequelize"; import { QueryTypes } from "sequelize"; import { z } from "zod/v4"; import type { ApplicationCoincidence, ApplicationCoincidenceQuery } from "../../../common"; import { ApplicationCoincidencesResponseSchema, GlobalApplicationSummarySchema, } from "../../../common"; import { compareApplicationProduct } from "../../application/application-coincidence"; import { ResourceReferenceError } from "../../application/resource-reference-service"; import { readResourceReferenceMetadata } from "./resource-reference-metadata"; import { ResourceScopeSchemaReader } from "./resource-scope-schema"; const CursorSchema = z.strictObject({ version: z.literal(1), context: z.string(), afterId: z.uuid(), }); function decodeCursor(cursor: string | undefined, context: string) { if (!cursor) { return Result.ok(undefined); } try { const parsed = CursorSchema.safeParse( JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")) ); return parsed.success && parsed.data.context === context ? Result.ok(parsed.data.afterId) : Result.fail(new ResourceReferenceError("INVALID_REQUEST")); } catch { return Result.fail(new ResourceReferenceError("INVALID_REQUEST")); } } function scanMatches( source: { name: string; provider: string | null }, rows: Awaited>, query: ApplicationCoincidenceQuery ) { const items: ApplicationCoincidence[] = []; let scanned = 0; let last: string | undefined; const search = normalizeSearchText(query.search); for (const row of rows.slice(0, 200)) { if (!row.reference) { throw new Error("Referencia global incompatible."); } last = row.reference.id; scanned++; const candidate = GlobalApplicationSummarySchema.parse({ ...row.reference, provider: row.provider, }); const match = compareApplicationProduct( { name: source.name, provider: source.provider }, candidate ); if ( match && (!search || normalizeSearchText(`${candidate.name} ${candidate.provider ?? ""}`).includes(search)) ) { items.push(match); } if (items.length === query.pageSize) { break; } } return { items, scanned, last }; } /** Lectura contextual y acotada: a lo sumo 200 productos globales comparados por petición, sin escrituras. */ export class ApplicationCoincidenceReader { private readonly schema: ResourceScopeSchemaReader; constructor( private readonly database: Sequelize, private readonly enabled: boolean ) { this.schema = new ResourceScopeSchemaReader(database); } async list(company: UniqueID, resource: UniqueID, query: ApplicationCoincidenceQuery) { return this.database.transaction(async (transaction) => { const exists = await this.database.query("SELECT id FROM companies WHERE id=:id", { replacements: { id: company.toPrimitive() }, transaction, type: QueryTypes.SELECT, }); if (!exists.length) { return Result.fail(new ResourceReferenceError("COMPANY_NOT_FOUND")); } const mode = await this.schema.read(); const own = ( await readResourceReferenceMetadata(this.database, transaction, mode, { companyId: company.toPrimitive(), ids: [resource.toPrimitive()], }) )[0]; if (!(own?.reference?.scope === "company" && own.reference.kind === "application")) { return Result.fail(new ResourceReferenceError("RESOURCE_REFERENCE_NOT_FOUND")); } if (!this.enabled) { return Result.fail(new ResourceReferenceError("RESOURCE_GLOBAL_FEATURE_DISABLED")); } const context = JSON.stringify([ company.toPrimitive(), resource.toPrimitive(), own.reference.name, own.provider, query.search, query.pageSize, ]); const decoded = decodeCursor(query.cursor, context); if (!decoded.isSuccess) { return decoded; } const afterId = decoded.data; const rows = await readResourceReferenceMetadata(this.database, transaction, mode, { globalOnly: true, limit: 201, ...(afterId ? { afterId } : {}), }); const { items, scanned, last } = scanMatches( { name: own.reference.name, provider: own.provider }, rows, query ); const complete = rows.length <= scanned; const nextCursor = !complete && last ? Buffer.from(JSON.stringify({ version: 1, context, afterId: last })).toString( "base64url" ) : null; return Result.ok( ApplicationCoincidencesResponseSchema.parse({ source: { ...own.reference, provider: own.provider }, items, nextCursor, complete, scanned, }) ); }); } }