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

pothos-plugin-collections

v1.0.0

Published

Pothos plugin exposing t.collection (paginated child lists with count) and t.element (single-value per-parent lookups), both with automatic per-request batching.

Readme

Collections plugin

Adds two Pothos field methods:

  • t.collection — paginated child list with a count companion.
  • t.element — at-most-one row per parent (no wrapper, no paging).

Both batch sibling resolutions across one request, so a list of N parents costs one loadItems call, not N.

Install

npm install pothos-plugin-collections

Batching is built on @pothos/plugin-dataloader, so make sure the peer dependencies are present:

npm install @pothos/core @pothos/plugin-dataloader graphql

Setup

Add CollectionsPlugin to your builder. It pulls in @pothos/plugin-dataloader for you, so you don't register that plugin yourself — just keep it installed (see above):

import SchemaBuilder from '@pothos/core';
import CollectionsPlugin from 'pothos-plugin-collections';

const builder = new SchemaBuilder({
  plugins: [CollectionsPlugin],
});

t.collection and t.element are then available on every field builder.

t.collection

builder.objectField(OrganizationType, 'users', (t) =>
  t.collection({
    name: 'OrganizationUsers',
    type: UserType,
    maxItems: 100,
    sort: UserSort,
    args: { emailContains: t.arg.string({ required: false }) },
    parentKey: (organization) => organization.id,
    itemKey: (user) => user.organizationId,
    loadItems: async (parentKeys, { skip, take, sort, emailContains }, ctx) => {
      // Return all matching rows for every parent key, in any order.
    },
    loadCount: async (parentKeys, { emailContains }, ctx) => {
      // Return one row per parent: { ...sameFieldsAsItemKey, count }.
      // Omit parents with no matches (they count as zero).
    },
  }),
);

The wrapper type has items: [Type] and count: Int. User filters live on the outer field; paging lives on items:

organization {
  users(emailContains: "@acme") {            # user args
    items(skip: 0, take: 10, sort: NAME_A_TO_Z) { id name }
    count
  }
}

t.element

builder.objectField(UserType, 'organization', (t) =>
  t.element({
    type: OrganizationType,
    nullable: false,                // default; throws a clear error if a parent has no row
    parentKey: (user) => user.organizationId,
    itemKey: (organization) => organization.id,
    load: async (keys, _args, ctx) => {
      // Return at most one row per key, in any order.
    },
  }),
);

Inputs

| Field | Shape | Notes | | --- | --- | --- | | parentKey | (parent) => Key | Same Key shape as itemKey. Use primitives, or arrays of primitives for composite keys — not class instances. The plugin throws at first load if shapes don't match. | | itemKey | (row) => Key | Reads enough of row to identify its parent. Must never return null / undefined. loadCount rows must include these fields. | | loadItems | (parentKeys, args, ctx) => Row[] | Flat list, any order. Honor skip/take per parent, not across the union. | | loadCount | (parentKeys, args, ctx) => { ...itemKeyFields, count }[] | One row per parent that has matches. | | load (element) | (parentKeys, args, ctx) => Row[] | Up to one row per key. | | args | Pothos arg map | Filters; passed to loadItems / loadCount / load. | | maxItems | number | Enables skip/take on items; take is required and must be 1..maxItems. | | sort | string[] or string-valued object | Enables an optional sort arg on items. | | sortName | string | Optional override for the generated sort-enum type name (default `${name}Sort`). | | nullable (element) | boolean | Defaults to false. When false, a missing row yields a clear plugin error per key instead of a generic GraphQL "Cannot return null". |

Errors from loadItems / loadCount / load are isolated per-group: if one user-args group fails, only its parents resolve to the error — sibling groups still resolve normally.

Sort

Define the allowed orderings as a string array or a string enum, and pass it as sort (this is the UserSort from the t.collection example above):

const UserSort = ['NEWEST_FIRST', 'NAME_A_TO_Z'] as const;
// or a string enum:
// enum UserSort { NEWEST_FIRST = 'NEWEST_FIRST', NAME_A_TO_Z = 'NAME_A_TO_Z' }
  • The sort arg is always optional in the schema — adding new values is non-breaking. When omitted, the resolver receives undefined; return a stable default.
  • Each enum value is a complete ordering (NEWEST_FIRST, NAME_A_TO_Z), not a field plus direction. Keeps invalid combinations out of the schema and maps directly to UI labels.
  • Enum names and values pass through verbatim.