npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

prisma-modelcore

v1.0.3

Published

Prisma + ModelCore, runtime data integrity for your database

Readme

A Prisma extension for ModelCore

Runtime entity integrity for your Prisma models.
Bridges Prisma with ModelCore so your entities stay validated from the database to the UI and back.

npm install prisma-modelcore @bufferpunk/modelcore @prisma/client

What it does

One Prisma client.

  • Hydrates on reads — results become modelcore instances. Validated and continuously enforced.
  • Updates (update, updateMany, upsert) — always field-validated via runValidate. Partial data, full safety.
  • Creates — pass a ModelCore instance or a plain object. Both work. The instance IS the data.
  • No flag — zero overhead. Plain Prisma.
Reads:
  prisma.user.findMany()                         → plain objects
  prisma.user.findMany({ hydrate: true })         → ModelCore instances

Updates (always validated per field):
  prisma.user.update({ where: { id: 1 }, data: { name: 'Bob' } })
  // name checked against schema — type, min, max, enum, ...

Creates:
  prisma.user.create({ data: User.createFrom({ name: 'Alice' }) })
  prisma.user.create({ data: { name: 'Alice' } })
  // both work — ModelCore instances are clean data, no conversion needed

Quick start

1. Add the generator to your Prisma schema

generator modelcore {
  provider = "prisma-modelcore"
}

Then run prisma generate. The generator reads the DMMF directly and writes ModelCore classes alongside your Prisma client:

src/models/generated/
├── index.ts          # exports all models + `registry` object
├── User.ts
├── Post.ts
├── Role.ts           # Prisma enums become TS enums
└── etc...

2. Wire up

import { PrismaClient } from '@prisma/client'
import { modelcoreExtension } from 'prisma-modelcore'
import { registry } from './models/generated'

const prisma = new PrismaClient().$extends(modelcoreExtension(registry))

3. Reads

// Plain — zero overhead
const users = await prisma.user.findMany()

// Hydrated — results are ModelCore instances
const users = await prisma.user.findMany({
  hydrate: true,
  include: { posts: true },
})

users[0].name            // "Alice"
users[0].name = 'Bob'    // validated by ModelCore — type, min, max, enum, ...

// Relations are recursively constructed via coercion in the schema
users[0].posts[0]        // Post ModelCore instance — validated on construction
users[0].posts[0].title  // String, validated

4. Creates

import { User } from './models/generated'

// ModelCore instance — works directly, no conversion needed
const user = new User({ name: 'Alice', email: '[email protected]' })
await prisma.user.create({ data: user })

// Plain object — also works
await prisma.user.create({ data: { name: 'Alice' } })

// With hydrate: true → wraps the returned result too
const created = await prisma.user.create({ data: user, hydrate: true })
// created is a User ModelCore instance

5. Updates — always validated

// Every field passes through runValidate against the schema
await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Bob' },
})
// throws if name fails type check, min, max, enum, etc.

// ModelCore instance also works
const update = new User({ name: 'Bob' })
await prisma.user.update({ where: { id: 1 }, data: update, hydrate: true })
// result is a User ModelCore instance

6. Validate without writing

const validated = prisma.user.validate({ name: 'Alice', email: '[email protected]' })
// Returns a User instance, or throws a ModelCoreError

How it works

Read path

No hydrate     ──> query() ──> plain Prisma result
hydrate: true  ──> query() ──> Model.createFrom(result)
                                └── coerce: true on relations → nested instances

Update path

For each key in the update data:
  present in schema?
    ├── yes ──> runValidate(conf, value, key, false)  ← throws on invalid
    └── no  ──> skip

Then: query(args)
Then: hydrate? → wrap result if yes

Create path

ModelCore instance or plain object → query(args) → result
No transformation needed — instances ARE the data.

hydrate: true controls result wrapping only

| Operation | No flag | hydrate: true | |---|---|---| | findUnique | T \| null | wrapped T \| null | | findMany | T[] | wrapped T[] | | create | T | wrapped T | | update | T | wrapped T | | upsert | T | wrapped T | | delete | T | wrapped T | | createMany | { count } | { count } | | count, aggregate | scalar | scalar |


Generated code

Model files (src/models/generated/User.ts)

import { Base, type SchemaDefinition } from '@bufferpunk/modelcore'
import { Post } from './Post'
import { RoleValues } from './Role'

export class User extends Base {
  private static _schema: SchemaDefinition | null = null;
  static override get schema(): SchemaDefinition {
    if (!User._schema) {
      User._schema = {
        id:    { type: Number, immutable: true, optional: true },
        name:  { type: String, min: 2, max: 80 },
        email: { type: String },
        role:  { type: String, enum: RoleValues, optional: true },
        posts: { type: Array, values: { type: Post, coerce: true }, optional: true },
        bio:   { type: String, optional: true },
        score: { type: Number, optional: true },
      } as const satisfies SchemaDefinition;
    }
    return User._schema;
  }
}

Key details:

  • Relation fields (like posts) get optional: true, coerce: true — when Prisma returns them via include, ModelCore constructs Post instances from raw data recursively.
  • @id fieldsimmutable: true, optional: true.
  • @default(autoincrement())optional: true.
  • DateTimecoerce: true for string→Date.

Type mappings:

| Prisma type | ModelCore type | |---|---| | String | String | | Int | Number | | Float | Number | | Boolean | Boolean | | DateTime | Date | | Json | Object | | Bytes | String | | BigInt | Number | | Decimal | Number |

Schema rules:

| Prisma attribute | Schema config | |---|---| | @id | immutable: true, optional: true | | @default(autoincrement()) | optional: true | | @default(uuid()) / @default(cuid()) | optional: true | | @updatedAt | default: () => new Date() | | ? (optional field) | optional: true | | Relation field (model type) | optional: true, coerce: true | | @default("value") | default: "value" | | @default(now()) | default: () => new Date() |

Registry (src/models/generated/index.ts)

export { User } from './User'
export { Post } from './Post'

import { User as _User } from './User'
import { Post as _Post } from './Post'

export const registry = {
  User: _User,
  Post: _Post,
} as const

export type Registry = typeof registry

Enums

For example src/models/generated/Role.ts

export enum Role {
  ADMIN = "ADMIN",
  USER = "USER",
}

export const RoleValues = ["ADMIN", "USER"] as const

API reference

modelcoreExtension(registry)

const prisma = new PrismaClient().$extends(modelcoreExtension(registry))

| Param | Type | Description | |---|---|---| | registry | Record<string, typeof Base> | Map of model name → ModelCore class (from generated index.ts) |

prisma.<model>.validate(data)

Validate data against a model's schema — no database round-trip.

const user = prisma.user.validate({ name: 'Alice', email: '[email protected]' })
// User instance or throws ModelCoreError

hydrate: true

Pass on any entity-returning query to wrap the result in ModelCore:

prisma.user.findMany({ hydrate: true })
prisma.user.findUnique({ where: { id: 1 }, hydrate: true })
prisma.user.create({ data: user, hydrate: true })
prisma.user.update({ where: { id: 1 }, data: update, hydrate: true })
prisma.user.upsert({ where: { id: 1 }, create, update, hydrate: true })
prisma.user.delete({ where: { id: 1 }, hydrate: true })

Design rationale

Why coerce: true on relation fields?

Generated schemas include relation fields with coerce: true and optional: true. When Prisma returns data with include, ModelCore's constructor calls new RelatedModel(data) for each item — recursively validating the entire tree. No separate wrapping step needed.

Why field-level validation on updates?

ModelCore's runValidate(conf, value, path, isNew) validates a single field against its schema config. Updates pass each key through runValidate individually — partial data like { name: 'Bob' } gets the same safety as full construction without requiring every field to be present.


Limitations

  • select queries: Partial results may omit required schema fields, causing Model instantiation to throw when hydrating. Use include for full wrapping, or make fields optional in your schema.
  • _count includes: Passed through as-is (not a model).
  • Raw queries: $queryRaw / $executeRaw have no model context — not intercepted.

Compatibility & Resiliency Features

  • Prisma 7 Ready: Fully supports standard JSON-RPC generator protocols.
  • Atomic Operations & Nested Relations: Automatically identifies database-level actions (e.g. { increment: 1 }, { connect: { id: 3 } }) during validation loops and safely bypasses them to avoid runtime schema mismatches.
  • Hydration Boundary Resilience: An runtime try/catch mechanism in result wrapping guards read pipelines. If a database record does not align with your schema class config, it prints a console warning and falls back to returning the raw database payload directly instead of crashing.

For more details on generator protocols, circular relationship resolution, and runtime interception flow, see Architecture and Design.


License

MIT © bufferpunk