> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Link Types

> Declare associations between records, with cardinality enforced on every write

A link type is an ordinary table whose rows associate two records. You declare
which two fields are the link's *endpoints* and how many links each record may
take part in, and Bijection enforces those bounds on every transaction.

```ts bijection/schema.ts {15-17} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineSchema, defineTable } from "bijection/server";
import { v } from "bijection/values";

export default defineSchema({
  customers: defineTable({ name: v.string() }),
  invoices: defineTable({
    reference: v.string(),
    amount_cents: v.int64(),
  }),
  customer_invoices: defineTable({
    customer_id: v.id("customers"),
    invoice_id: v.id("invoices"),
    note: v.optional(v.string()),
  })
    .link({ customer_id: [0, "many"], invoice_id: [0, 1] })
    .index("by_customer_invoice", ["customer_id", "invoice_id"])
    .index("by_invoice", ["invoice_id"]),
});
```

Each `customer_invoices` document is one link between a customer and an
invoice. A customer can have any number of invoices, and an invoice belongs to
at most one customer.

## Declaring a link type

Call `.link` on a table definition with an object that has exactly two entries.
Each key is an endpoint field and each value is its cardinality,
`[minimum, maximum]`:

* The endpoint fields must be required, top-level `v.id(...)` fields of the
  table. System tables can't be endpoints.
* The other fields of the table, such as `note` above, are ordinary data about
  the link. An ID field that isn't named in `.link` isn't an endpoint.
* A table can have one `.link` declaration.

`.link` can't be used on a view, on a table filled by an
[integration](/integrations/overview), or on a table with a staged validator.

## Cardinality

An endpoint's bounds count the live links that each document of the endpoint's
table takes part in. In the example, `invoice_id: [0, 1]` means every invoice
is in zero or one `customer_invoices` link.

| Bounds | Each record takes part in |
| - | - |
| `[0, 1]` | At most one link |
| `[1, 1]` | Exactly one link |
| `[0, "many"]` | Any number of links |
| `[1, "many"]` | At least one link |
| `[2, 5]` | Between two and five links |

`"many"` means no upper bound. Bounds are non-negative integers with the
minimum no greater than the maximum.

Put a bound on the endpoint it constrains. "An invoice has at most one customer"
is a bound on `invoice_id`, not on `customer_id`.

### Minimums

A minimum applies to every document in the endpoint's table, including
documents that have no links yet. With `invoice_id: [1, 1]`, an invoice can't
exist without its link, so a mutation that inserts an invoice must also insert
its link:

```ts bijection/invoices.ts {11-14} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { mutation } from "./_generated/server";
import { v } from "bijection/values";

export const create = mutation({
  args: { customer_id: v.id("customers"), reference: v.string() },
  handler: async (ctx, args) => {
    const invoice_id = await ctx.db.insert("invoices", {
      reference: args.reference,
      amount_cents: 0n,
    });
    await ctx.db.insert("customer_invoices", {
      customer_id: args.customer_id,
      invoice_id,
    });
    return invoice_id;
  },
});
```

Use a minimum of `0` when records can legitimately exist before they're
associated, and track unassigned records with an ordinary query.

## Creating, changing and ending links

Links are written with ordinary [mutations](/functions/mutation-functions):

* **Create** a link with `db.insert`.
* **Change its data** with `db.patch` or `db.replace`, as long as both endpoint
  fields keep their values.
* **End** a link with `db.delete`.

Endpoints can't be changed in place. To move an invoice to another customer, end
the old link and create the new one in the same mutation:

```ts bijection/invoices.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { mutation } from "./_generated/server";
import { v } from "bijection/values";

export const reassign = mutation({
  args: { invoice_id: v.id("invoices"), customer_id: v.id("customers") },
  handler: async (ctx, args) => {
    const current = await ctx.db
      .query("customer_invoices")
      .withIndex("by_invoice", (q) => q.eq("invoice_id", args.invoice_id))
      .unique();
    if (current !== null) {
      await ctx.db.delete("customer_invoices", current._id);
    }
    await ctx.db.insert("customer_invoices", {
      customer_id: args.customer_id,
      invoice_id: args.invoice_id,
    });
  },
});
```

A pair of records has at most one live link. If you end a link and later insert
the same pair again, the restored link gets back its original `_id` and
`_creationTime`.

### Checked on the final state

Bijection checks links against the state the whole transaction leaves behind,
not after each write. That's why `reassign` can briefly leave the invoice
without a link, and why a mutation can delete a customer together with all its
links. It also covers writes that don't touch the link table: deleting a
customer that still has live links fails, even though no link was written.

A transaction that breaks a link rule fails with one of these errors, and none
of its writes are committed:

| Error | Cause |
| - | - |
| `LinkCardinality` | A record would take part in more links than its maximum, or fewer than its minimum. |
| `LinkEndpointMissing` | A live link would point at a document that doesn't exist. |
| `LinkEndpointImmutable` | A `patch` or `replace` changed an endpoint field. |
| `LinkAlreadyExists` | The same pair of records already has a live link. |
| `LinkValidationBudget` | Checking the transaction's links exceeded the work Bijection allows for link checks in one transaction. |

Declaring a link type, or making its bounds stricter, also checks the documents
already in your tables when you deploy.

## Indexes

To check links efficiently, a link type needs two indexes. Taking the endpoint
fields in alphabetical order, one index begins with both fields and the other
begins with the second field. Bijection adds these as `by_link_pair` and
`by_link_endpoint_2` unless you've declared indexes that begin with the same
fields, in which case it reuses yours.

Declare the indexes you query yourself, so that they have meaningful names and
appear in your generated types. In the example at the top of this page,
`by_customer_invoice` begins with both endpoints and `by_invoice` begins with
`invoice_id`, so Bijection adds no indexes of its own.

## Reading links

A link type is a table, so you read links with ordinary
[indexed queries](/database/reading-data/indexes/indexes), in either direction:

```ts bijection/invoices.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { query } from "./_generated/server";
import { v } from "bijection/values";

// From a customer to its invoices.
export const forCustomer = query({
  args: { customer_id: v.id("customers") },
  handler: async (ctx, args) => {
    const links = await ctx.db
      .query("customer_invoices")
      .withIndex("by_customer_invoice", (q) =>
        q.eq("customer_id", args.customer_id),
      )
      .take(100);
    return Promise.all(
      links.map((link) => ctx.db.get("invoices", link.invoice_id)),
    );
  },
});

// From an invoice to its customer.
export const customerOf = query({
  args: { invoice_id: v.id("invoices") },
  handler: async (ctx, args) => {
    const link = await ctx.db
      .query("customer_invoices")
      .withIndex("by_invoice", (q) => q.eq("invoice_id", args.invoice_id))
      .unique();
    return link === null ? null : await ctx.db.get("customers", link.customer_id);
  },
});
```

`.unique()` is safe in `customerOf` because `invoice_id` has a maximum of `1`.

### Links in views

You can also traverse links inside a [view](/views/defining-views). Because an
invoice has at most one link, a view keyed by invoices can join it with the
default `cardinality: "one"`:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
invoice_details: defineView({
  key: { from: "invoices", field: "_id" },
  expression: q
    .table("invoices")
    .as("invoice")
    .leftJoin(q.table("customer_invoices").as("link"), {
      left: "invoice._id",
      right: "link.invoice_id",
      index: "by_invoice",
    })
    .select({
      reference: q.field("invoice.reference"),
      customer_id: q.field("link.customer_id"),
    }),
}),
```

In the other direction, group the links by customer to count each customer's
invoices:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const invoiceCounts = q
  .table("customer_invoices")
  .groupBy(["customer_id"])
  .aggregate({ invoice_count: q.count() })
  .as("invoices");

customer_invoice_counts: defineView({
  key: { from: "customers", field: "_id" },
  expression: q
    .table("customers")
    .as("customer")
    .leftJoin(invoiceCounts, {
      left: "customer._id",
      right: "invoices.customer_id",
    })
    .select({
      name: q.field("customer.name"),
      invoice_count: q.field("invoices.invoice_count"),
    }),
}),
```

Reads of links, directly or through views, follow the ordinary
[consistency and reactivity](/views/reading-views#consistency-and-reactivity)
rules: a query that follows a link updates when the link is created, ended or
reassigned.

## Links in the console

<Warning>Links in the console schema view are in beta.</Warning>

The [Schema page](/dashboard/deployments/schema) of the console draws each
declared link as edges from the link table to its two endpoint tables. Selecting
a table lists the links it takes part in, the table that declares each one, and
each endpoint's bounds, written like `0..1`, `1` or `0..*`. For a table that
declares a link, the diagram shows the declared endpoints instead of guessing
edges from its ID fields.
