Prisma adapter
@smonn/ids/prisma provides a read/write transform pair for integrating
Id<Brand> with Prisma’s $extends extension model. It requires
@prisma/client ≥ 7.0.0 as an optional peer dependency.
pnpm add @prisma/client@">=7"Basic usage
Section titled “Basic usage”import { idField } from "@smonn/ids/prisma";import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");const userIdField = idField(usr);
const xprisma = prisma.$extends({ result: { user: { id: userIdField.computeField("id") }, },});// xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
// Write path: write validates via codec.safeParse and returns the canonical stringawait xprisma.user.create({ data: { id: userIdField.write(usr.generate()), name: "Alice" } });idField(codec) requires IdGeneratingCodec — a codec variant exposing a synchronous generate(). Only the Timestamp codec and Reverse Timestamp codec satisfy this constraint; the Opaque, Signed, Wrapped, and Digest codecs do not expose a synchronous generate() and cannot be passed to idField(). For those codecs, use idFieldNonGenerating instead.
- Write path:
writevalidates the value viacodec.safeParsebefore passing it to the driver. A cast-smuggled or otherwise invalid string throwsIdsError("invalid_id")at write time. Passingnullorundefinedalso throws. - Read path: values are normalised via
codec.safeParse(). An unrecognised value throws at read time so corrupt data surfaces immediately.
Auto-generating IDs on create — defaultQuery
Section titled “Auto-generating IDs on create — defaultQuery”Pair defaultQuery with computeField in a $extends block to have IDs auto-generated on write and correctly typed on read, without touching every call site:
import { idField } from "@smonn/ids/prisma";import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");const userIdField = idField(usr);
const xprisma = prisma.$extends({ query: { user: userIdField.defaultQuery("id") }, result: { user: { id: userIdField.computeField("id") } },});
// id is auto-filled on create, and typed as Id<"usr"> on readawait xprisma.user.create({ data: { name: "Alice" } });The schema keeps a plain String @id with no @default(…); the extension supplies the value client-side.
defaultQuery intercepts create, createMany, createManyAndReturn, and upsert:
create— when the field is absent,undefined, ornullinargs.data, injects a freshly generatedId<Brand>; when the field is present, validates it viacodec.safeParseand throwsIdsError("invalid_id")if invalid.createMany— handles both the array and single-object forms of Prisma’sEnumerable<T>: iterates each element whenargs.datais an array; applies the same absent-injects / present-validates logic directly whenargs.datais a single object; an invalid ID throwsIdsError("invalid_id").createManyAndReturn— identical branching tocreateMany: array form maps each item through inject-or-validate; single-object form applies the same absent-injects / present-validates logic directly; absentargs.datais passed through unchanged; an invalid ID throwsIdsError("invalid_id").upsert— applies the same logic toargs.create(the new-row data); theupdateside is left unchanged.
Non-generating path for codecs without synchronous generate() — idFieldNonGenerating
Section titled “Non-generating path for codecs without synchronous generate() — idFieldNonGenerating”Codecs that do not expose a synchronous generate() — the Opaque Timestamp, Signed Timestamp, Wrapped key, and Digest codecs — cannot be passed to idField(). Use idFieldNonGenerating for those variants. It accepts any IdColumnCodec (only safeParse is required) and returns the same read/transform surface as idField minus defaultQuery:
import { idFieldNonGenerating } from "@smonn/ids/prisma";import { createOpaqueTimestampId, importOpaqueKey } from "@smonn/ids/opaque";
const key = await importOpaqueKey(rawKeyBytes);const inv = createOpaqueTimestampId("inv", { key });const invoiceIdField = idFieldNonGenerating(inv);
const xprisma = prisma.$extends({ result: { invoice: { id: invoiceIdField.computeField("id") }, },});// xprisma.invoice.findUnique(…).id is typed as Id<"inv"> — no cast requiredidFieldNonGenerating returns read, readNullable, write, computeField, and computeNullableField — identical in behaviour to their idField counterparts. It does not return defaultQuery; that method requires generate(), which these codecs do not provide. The omission is enforced at the TypeScript type level: the return type is Omit<IdTransform<Brand>, "defaultQuery">.
The name reflects the provenance axis: this mapper does not generate IDs; it parses and serialises a caller-supplied value. It is not read-only — the return value includes a write method.
If you need defaultQuery (auto-generating IDs on create/createMany/upsert), use idField with a Timestamp or Reverse Timestamp codec instead.
Nullable columns
Section titled “Nullable columns”Both idField(...) and idFieldNonGenerating(...) expose readNullable and computeNullableField for optional foreign keys. Use computeNullableField in a $extends result block and readNullable for inline reads.
computeNullableField in a $extends block
Section titled “computeNullableField in a $extends block”import { idField } from "@smonn/ids/prisma";import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");const userIdField = idField(usr);const pst = createTimestampId("pst");const postIdField = idField(pst);
const xprisma = prisma.$extends({ result: { post: { // non-nullable primary key id: postIdField.computeField("id"), // nullable optional FK — author may be null authorId: userIdField.computeNullableField("authorId"), }, },});
// xprisma.post.findUnique(…).authorId is typed as Id<"usr"> | nullconst post = await xprisma.post.findUnique({ where: { id: someId } });console.log(post?.authorId); // Id<"usr"> | nullreadNullable for inline reads
Section titled “readNullable for inline reads”const authorId = userIdField.readNullable(rawRow.authorId);// authorId is Id<"usr"> | null — null when rawRow.authorId is null or undefinedreadNullablereturnsnullwhen the value isnullorundefined; for any other value it delegates to the samesafeParse-based path asreadand throwsIdsError("invalid_id")on failure.computeNullableField(fieldName)produces a$extendsresult-component field whosecomputefunction returnsId<Brand> | null, correctly typed through Prisma’s type machinery without a per-call-site cast.
Both helpers are available on both idField(...) and idFieldNonGenerating(...) return values.
nullableIdField — standalone nullable mapper
Section titled “nullableIdField — standalone nullable mapper”For nullable FK columns that only need read/write transforms and no computeField/defaultQuery, use nullableIdField:
import { nullableIdField } from "@smonn/ids/prisma";import { createTimestampId } from "@smonn/ids";
const usr = createTimestampId("usr");const authorIdField = nullableIdField(usr);
// Write: null/undefined → null; valid Id<Brand> → canonical string; invalid → throwsauthorIdField.write(null); // → null (FK clear)authorIdField.write(undefined); // → null (FK clear)authorIdField.write(validId); // → canonical stringauthorIdField.write("bad" as Id<"usr">); // throws IdsError("invalid_id")nullableIdField(codec) returns readNullable, write, and computeNullableField — it is equivalent to the nullable surface of idField/idFieldNonGenerating but without the non-nullable read path or defaultQuery. The write method accepts Id<Brand> | null | undefined and returns string | null, matching the nullable FK clear pattern used by the Drizzle, Kysely, MikroORM, and TypeORM adapters.
Error handling
Section titled “Error handling”The read path throws IdsError with code "invalid_id" when the stored value does not parse
as a valid Id<Brand>. The underlying ParseError is attached as err.cause. Catch and
narrow using isIdsError:
import { idField, isIdsError } from "@smonn/ids/prisma";
try { const id = userIdField.read(user.id);} catch (err) { if (isIdsError(err) && err.code === "invalid_id") { // err.cause is the ParseError returned by safeParse }}IdsError, isIdsError, and IdsErrorCode are re-exported from @smonn/ids/prisma — no
separate import from "@smonn/ids" is needed. For the full list of IdsErrorCode values, see
the error-code reference.