prisma-ltree

Authoring Contracts

Declare ltree columns in the PSL lane or the TypeScript lane; both produce the same contract

Prisma Next gives you two ways to author your data contract, and prisma-ltree supports both:

  • the PSL lane: contract.prisma, the surface the official extensions document
  • the TypeScript lane: contract.ts via defineContract

Both produce the same compiled contract. Pick whichever surface fits your project and get exactly the same result.

Composing the extension

Whichever lane you author in, add ltree to extensions in prisma-next.config.ts so the ltree namespace resolves during emit:

prisma-next.config.ts
import { defineConfig } from "@prisma-next/postgres/config";
import ltree from "prisma-ltree/control";

export default defineConfig({
  contract: "./contract.prisma", // or "./contract.ts"
  extensions: [ltree],
});

Omitting ltree from extensions while a contract.prisma references ltree.Ltree() fails emit with PSL_EXTENSION_NAMESPACE_NOT_COMPOSED.

Declaring ltree columns

The extension gives you two named-type constructors: ltree.Ltree() for a single ltree path (codec pg/ltree@1) and ltree.LtreeArray() for an ltree[] array (codec pg/ltree-array@1). Author them in either lane:

contract.prisma
// use prisma-next

types {
  // Single ltree path → codec `pg/ltree@1`, native type `ltree`.
  Path = ltree.Ltree()
  // ltree[] array → codec `pg/ltree-array@1`, native type `ltree[]`.
  Paths = ltree.LtreeArray()
}

/// A page in a hierarchy. `path` is its position in the tree;
/// `breadcrumbs` is the array of ancestor paths.
model Page {
  id          String @id @default(uuid())
  path        Path
  breadcrumbs Paths

  @@map("page")
}

The parentheses are required, even though these constructors take no arguments. Path = ltree.Ltree (no parens) fails with PSL_INVALID_TYPES_MEMBER.

contract.ts
import { defineContract } from "@prisma-next/postgres/contract-builder";
import ltree from "prisma-ltree/pack";

export const contract = defineContract(
  {
    extensionPacks: { ltree },
  },
  ({ field, model, type }) => {
    const types = {
      // Single ltree path → codec `pg/ltree@1`, native type `ltree`.
      Path: type.ltree.Ltree(),
      // ltree[] array → codec `pg/ltree-array@1`, native type `ltree[]`.
      Paths: type.ltree.LtreeArray(),
    } as const;

    const Page = model("Page", {
      fields: {
        id: field.id.uuidv4String(),
        path: field.namedType(types.Path),
        breadcrumbs: field.namedType(types.Paths),
      },
    });

    return {
      types,
      models: { Page: Page.sql({ table: "page" }) },
    };
  },
);

export default contract;

The ltree namespace on type is the same authoring surface the PSL lane exposes as ltree.Ltree() / ltree.LtreeArray().

After editing either contract, re-emit:

pnpm prisma-next contract emit

Next steps

Once your columns are declared, see Getting Started for runtime wiring, the baseline migration, and your first queries.

On this page