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

@node-media-library/prisma

v1.0.0

Published

Prisma adapter (MediaRepository + cascading deletes) for @node-media-library/core.

Readme

@node-media-library/prisma

Prisma adapter for @node-media-library/core. Pre-release: not yet published to npm.

Install

Once published: npm install @node-media-library/prisma @prisma/client @prisma/client (>=6 <8) is an optional peer dependency — bring your own version.

Prisma version compatibility

The peer range is >=6 <8. The adapter is structurally typed — it imports nothing from @prisma/client and talks to a { media: { findMany, create, ... } } shape you pass in — so it does not depend on any one client major.

CI exercises Prisma 7 only. Support for Prisma 6 rests on that import-graph property, not on a passing test suite. If you hit an incompatibility on 6, please open an issue; it will be treated as a bug in this range, not as unsupported usage.

Add the model

Paste into schema.prisma, then run your own migrate flow (prisma migrate dev / db push):

model Media {
  id                   String   @id
  modelType            String   @map("model_type")
  modelId              String   @map("model_id")
  uuid                 String   @unique
  collectionName       String   @map("collection_name")
  name                 String
  fileName             String   @map("file_name")
  mimeType             String?  @map("mime_type")
  disk                 String
  conversionsDisk      String?  @map("conversions_disk")
  // size Int caps individual files at ~2GB. Raising it is a library change, not a
  // schema-only one: MediaRow and core's MediaRecord both type size as number.
  size                 Int
  manipulations        Json
  customProperties     Json     @map("custom_properties")
  generatedConversions Json     @map("generated_conversions")
  responsiveImages     Json     @map("responsive_images")
  orderColumn          Int?     @map("order_column")
  createdAt            DateTime @default(now()) @map("created_at")
  updatedAt            DateTime @updatedAt @map("updated_at")

  @@index([modelType, modelId, collectionName])
  @@map("media")
}

Also exported verbatim as MEDIA_MODEL_SNIPPET. Prisma 7 note: it generates the client into your own output dir — pass that instance into prismaAdapter, don't assume a package default.

size Int caps individual files at ~2GB. Raising it is a library change, not a schema-only one: MediaRow and core's MediaRecord both type size as number, so changing the column alone would hand bigint to code expecting number.

Ordering for a model's media uses orderBy: [{ orderColumn: { sort: 'asc', nulls: 'last' } }, { createdAt: 'asc' }]. The nulls: 'last' behavior is verified against SQLite in this repo's test suite; run the exported contract suite against your own Postgres/MySQL before relying on it there.

iterateAll({ collectionName }) filters on collectionName without modelType, which the [modelType, modelId, collectionName] index cannot serve. iterateAll keyset-paginates by primary key (orderBy: { id: 'asc' }), so the planner typically walks id order and filters out non-matching rows rather than seeking through an index — each batch costs work proportional to table size, not to matches. That is fine at the scale clean runs today; add a [collectionName] index if you run it against a large table.

Usage

import { createMediaLibrary } from '@node-media-library/core'
import { prismaAdapter } from '@node-media-library/prisma'
import { PrismaClient } from './generated/prisma/client.js'
const prisma = new PrismaClient()
const media = createMediaLibrary({
  repository: prismaAdapter(prisma),
  storage: { disks: { default: { driver: 'fs', root: './storage' } } },
  models: { User: {} },
})

Cascading deletes (opt-in)

import { withMediaCascade } from '@node-media-library/prisma'
const xprisma = withMediaCascade(prisma, media)
await xprisma.user.delete({ where: { id: 'u1' } })

Cascaded models must expose a scalar id field — the extension reads result.id (delete) or each matched row's id (deleteMany) to call clearFor.

Options

prismaAdapter(client, { owners, iterateBatchSize }): owners is a modelType -> (modelId) => boolean | Promise<boolean> map, needed only by the future clean --delete-orphaned command (Plan 6) — most integrations can omit it. iterateBatchSize (default 100) sets the page size iterateAll fetches internally.

Filtering by customProperties

iterateAll({ customProperties: { storeId: 's1' } }) matches records whose customProperties contain every supplied key with a deep-equal value.

By default the filter runs in the application, not the database. The adapter passes modelType and collectionName into the SQL where clause, then discards non-matching rows in Node for anything not expressed there. Only modelType gets an index seek: the schema's @@index([modelType, modelId, collectionName]) is a composite index keyed leftmost by modelType, and iterateAll never filters on modelId, so the leftmost-prefix rule stops the index lookup at modelTypecollectionName is checked as a residual filter within that scan, not through the index. Either way, it reads every row matching the other filters.

To push customProperties into SQL, name your database's JSON path dialect:

prismaAdapter(client, { jsonPathStyle: 'postgres' }) // or 'mysql'

Prisma's JSON path operand differs per connector — PostgreSQL takes an array (['storeId']), MySQL and SQLite take a string ('$.storeId') — and this adapter never imports @prisma/client, so it cannot detect your provider. Naming the wrong one surfaces as a PrismaClientValidationError on the first filtered call, not as a type error.

Only scalar values are pushed into SQL, even with jsonPathStyle set. A key is pushed down only when its value is a string, number, or boolean — the cases where Prisma's JSON equals is unambiguous. Objects, arrays, and null are always filtered in the application instead, using the same deep-equality check the default path uses (Prisma's equals is not guaranteed to match that for non-scalars, and null in particular usually needs a Prisma.JsonNull/DbNull sentinel rather than a literal null). A filter mixing scalar and non-scalar keys still returns correct results, but the non-scalar keys get no performance benefit — the adapter still scans every row that matched the pushed-down keys to check them.

A key is also held back unless it's a simple identifier (/^[A-Za-z_$][A-Za-z0-9_$]*$/). The SQL path is built by interpolating the key directly (`$.${key}`), so a key containing ., ", [, or whitespace — 'shopify.storeId', say — would silently change which path is queried rather than erroring. Such keys always fall back to application-side filtering, same as a non-scalar value, and the same "still returns correct results, no performance benefit" tradeoff applies.

Even pushed down, JSON matching is unindexed by default. On PostgreSQL, add an expression index for the key you filter on:

CREATE INDEX media_store_id_idx ON "Media" ((("customProperties" ->> 'storeId')));

What CI exercises: the portable default path and jsonPathStyle: 'mysql', both against SQLite. The 'postgres' array form is asserted from Prisma's documented behavior, not from a passing test.

Responsive images

The repository exposes two additional MediaRepository methods backing @node-media-library/core's responsive images support: markConversionGenerated(id, name, generated) and mergeResponsiveImages(id, conversion, entry). Both read-merge-write into the generatedConversions / responsiveImages JSON columns respectively, keyed by conversion name — a plain update() would clobber sibling keys written concurrently, which is why these merge instead of replace.

When your PrismaLikeClient stub (or the real PrismaClient) exposes $transaction, the read-modify-write runs inside it; without one it falls back to a plain sequential read then write. Note that $transaction alone does not make concurrent merges on the same record fully atomic on Postgres/MySQL: their default isolation level (read committed) does not lock the row read by findUnique, so two concurrent transactions can both read the same pre-update row and, when both commit, one write can still be lost. SQLite serializes all writes (single-writer), so it does not have this gap — the "two concurrent calls for different names must both persist" contract from @node-media-library/core's MediaRepository JSDoc holds there, but is not proven for Postgres/MySQL under $transaction alone.

Roadmap

Orphan-cleanup CLI (clean --delete-orphaned) lands in Plan 6, building on owners above.