import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core"; import type { ResolvedVia } from "@rebasepro/types"; import { DrizzleClient } from "../interfaces"; import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry"; /** * Writing a many-to-many means writing rows in a junction table, and doing that * needs three things: the table, the column naming the row being written from, * and the column naming the far side. * * Those three used to be re-derived at each of the four call sites — twice for * `through`, twice for `joinPath`, in the owning and the inverse direction — * and the two `joinPath` derivations did not agree. The inverse one keyed off * `step.table`; the owning one asked `getTableNamesFromColumns` which table a * step's columns belonged to, and that answers `""` for an unqualified column * name. So for `{ table: "posts_tags", on: { from: "id", to: "tag_id" } }` — * the form the docs and every fixture use — no branch matched, both columns * stayed null, and the write was skipped with a warning nobody reads. Writing * a to-many `via` relation did nothing at all. * * Hence one binder, keyed on `step.table` (always present) and accepting the * qualified `table.column` form where it is used. */ export interface JunctionBinding { table: PgTable; /** The junction column holding the id of the row being written from. */ parentColumn: AnyPgColumn; /** The junction column holding the id of the row on the far side. */ targetColumn: AnyPgColumn; /** `.`, for error messages. */ label: string; } /** The junction a `manyToMany` names outright. */ export declare function bindThroughJunction(registry: PostgresCollectionRegistry, through: { table: string; sourceColumn: string; targetColumn: string; }, label: string): JunctionBinding; /** * The junction a `via` relation reaches through, from either end. * * A step is `{ table: T, on: { from, to } }` where `from` names a column on * whatever the walk was standing on and `to` names one on `T`. That positional * meaning is what makes the unqualified form work at all, so the walk carries * the previous table rather than asking the column names where they live. */ export declare function bindJoinPathJunction(registry: PostgresCollectionRegistry, joinPath: ResolvedVia["joinPath"], parentTableName: string, targetTableName: string, label: string): JunctionBinding; /** * Remove one link, leaving the row on the far side alone. * * This is what `DELETE authors/1/tags/5` has to mean for a many-to-many: the * target is shared, so deleting the row would remove the tag from every other * post that uses it. */ export declare function removeJunctionLink(tx: DrizzleClient, binding: JunctionBinding, parentId: unknown, targetId: unknown, subject: { parent: string; relation: string; }): Promise; /** * Make the junction say that `parentId` is linked to exactly `targetIds`, by * diffing against what is linked now rather than replacing the set. * * A save of the parent used to delete every junction row for it and re-insert * the ids the browser sent — a list the browser assembled from a read it did * earlier. Three things followed, all data loss rather than display: * * - **Lost update.** Two editors with post 7 open: A adds tag X and saves, B * saves any field from a form that predates it, and X is gone with nothing * reported to either of them. * - **A partially-read set is a partially-deleted set.** The read that fills * the form runs under RLS, so a user who may edit the parent but cannot see * some of the linked rows gets a shorter list — and writing it back deleted * the links they were never shown. The select that drives the diff runs in * this same transaction under the same policies, so a link the caller cannot * read is in neither list and survives the save. * - **Junction payload columns.** A junction carrying its own columns * (`position`, `role`, `created_at`) lost them on every save, because every * row was re-inserted with only the two keys. Untouched links are left alone. * * The insert is `ON CONFLICT DO NOTHING`, so two sessions adding the same link * concurrently is a no-op rather than a unique violation. */ export declare function applyJunctionMembership(tx: DrizzleClient, binding: JunctionBinding, parentId: unknown, targetIds: unknown[]): Promise;