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

@opensaas/stack-core

v0.18.2

Published

Core stack for OpenSaas - schema definition, access control, and runtime utilities

Readme

@opensaas/stack-core

Core OpenSaas Stack - config system, field types, access control, and code generation.

Installation

pnpm add @opensaas/stack-core

Features

  • 📝 Schema Definition - Config-first approach to defining your data model
  • 🔒 Access Control - Automatic enforcement at database layer
  • 🎯 Type Generation - Generate TypeScript types and Prisma schema
  • 🔄 Field Types - Extensible field type system
  • 🪝 Hooks - Data transformation and validation lifecycle
  • 🛡️ AI-Safe - Silent failures prevent information leakage

Quick Start

1. Define Your Schema

Create opensaas.config.ts:

import { config, list } from '@opensaas/stack-core'
import { text, integer, select, relationship } from '@opensaas/stack-core/fields'
import type { AccessControl } from '@opensaas/stack-core'

const isSignedIn: AccessControl = ({ session }) => !!session

const isAuthor: AccessControl = ({ session }) => {
  if (!session) return false
  return { authorId: { equals: session.userId } }
}

export default config({
  db: {
    provider: 'postgresql',
    url: process.env.DATABASE_URL,
  },
  lists: {
    User: list({
      fields: {
        name: text({ validation: { isRequired: true } }),
        email: text({ isIndexed: 'unique' }),
        posts: relationship({ ref: 'Post.author', many: true }),
      },
    }),
    Post: list({
      fields: {
        title: text({ validation: { isRequired: true } }),
        slug: text({ isIndexed: 'unique' }),
        content: text(),
        status: select({
          options: [
            { label: 'Draft', value: 'draft' },
            { label: 'Published', value: 'published' },
          ],
          defaultValue: 'draft',
        }),
        author: relationship({ ref: 'User.posts' }),
        internalNotes: text({
          access: {
            read: isAuthor,
            create: isAuthor,
            update: isAuthor,
          },
        }),
      },
      access: {
        operation: {
          query: ({ session }) => {
            if (!session) return { status: { equals: 'published' } }
            return true
          },
          create: isSignedIn,
          update: isAuthor,
          delete: isAuthor,
        },
      },
    }),
  },
})

2. Generate Schema and Types

opensaas generate

This creates:

  • prisma/schema.prisma - Prisma schema
  • .opensaas/types.ts - TypeScript types

3. Create Context

// lib/context.ts
import { getContext } from '@opensaas/stack-core'
import { PrismaClient } from '@prisma/client'
import config from '../opensaas.config'

export const prisma = new PrismaClient()

export async function getContextWithUser(userId: string) {
  return getContext(config, prisma, { userId })
}

export async function getContext() {
  return getContext(config, prisma, null)
}

4. Use in Your App

import { getContextWithUser } from './lib/context'

export async function createPost(userId: string, data: any) {
  const context = await getContextWithUser(userId)

  // Access control automatically enforced
  const post = await context.db.post.create({ data })

  if (!post) {
    return { error: 'Access denied' }
  }

  return { post }
}

Field Types

Available Fields

  • text() - String field
  • integer() - Number field
  • checkbox() - Boolean field
  • timestamp() - Date/time field
  • password() - Password field (excluded from reads)
  • select() - Enum field with options
  • relationship() - Foreign key relationship
  • json() - JSON field for arbitrary data
  • virtual() - Computed field not stored in database

Field Options

All fields support:

text({
  validation: {
    isRequired: true,
    length: { min: 3, max: 100 },
  },
  isIndexed: 'unique', // or true for non-unique index
  defaultValue: 'Hello',
  access: {
    read: ({ session }) => !!session,
    create: ({ session }) => !!session,
    update: ({ session }) => !!session,
  },
  hooks: {
    resolveInput: async ({ resolvedData }) => resolvedData,
    validateInput: async ({ operation, resolvedData }) => {
      if (operation === 'delete') return
      /* validate */
    },
  },
  ui: {
    fieldType: 'custom', // Reference global component
    component: CustomComponent, // Or provide directly
  },
})

Creating Custom Field Types

Field types are fully self-contained:

import type { BaseFieldConfig } from '@opensaas/stack-core'
import { z } from 'zod'

export type MyCustomField = BaseFieldConfig & {
  type: 'myCustom'
  customOption?: string
}

export function myCustom(options?: Omit<MyCustomField, 'type'>): MyCustomField {
  return {
    type: 'myCustom',
    ...options,
    getZodSchema: (fieldName, operation) => {
      return z.string().optional()
    },
    getPrismaType: (fieldName) => {
      return { type: 'String', modifiers: '?' }
    },
    getTypeScriptType: () => {
      return { type: 'string', optional: true }
    },
  }
}

Access Control

Operation-Level Access

Control who can query, create, update, or delete:

access: {
  operation: {
    query: true,  // Everyone can read
    create: isSignedIn,  // Must be signed in
    update: isAuthor,  // Only author
    delete: isAuthor,  // Only author
  }
}

Filter-Based Access

Return Prisma filters to scope access:

const isAuthor: AccessControl = ({ session }) => {
  if (!session) return false
  return { authorId: { equals: session.userId } }
}

// Applied as: where: { AND: [userFilter, { authorId: { equals: userId } }] }

Field-Level Access

Control access to individual fields:

internalNotes: text({
  access: {
    read: isAuthor, // Only author can see
    create: isAuthor, // Only author can set on create
    update: isAuthor, // Only author can modify
  },
})

Silent Failures

Access-denied operations return null or [] instead of throwing:

const post = await context.db.post.update({
  where: { id: postId },
  data: { title: 'New Title' },
})

if (!post) {
  // Either doesn't exist OR user lacks access
  // No information leaked about which
  return { error: 'Not found' }
}

Hooks

Transform and validate data during operations:

hooks: {
  // Transform input before validation
  resolveInput: async ({ resolvedData, operation, session }) => {
    if (operation === 'create') {
      return { ...resolvedData, createdBy: session.userId }
    }
    return resolvedData
  },

  // Custom validation
  validateInput: async ({ operation, resolvedData, fieldPath }) => {
    if (operation === 'delete') return
    if (resolvedData.title?.includes('spam')) {
      throw new Error('Title contains prohibited content')
    }
  },

  // Before database operation
  beforeOperation: async ({ operation, resolvedData }) => {
    console.log(`About to ${operation}`, resolvedData)
  },

  // After database operation
  afterOperation: async ({ operation, item }) => {
    if (operation === 'create') {
      await sendNotification(item)
    }
  },
}

Hook Execution Order

  1. resolveInput - Transform input
  2. validateInput - Custom validation
  3. Field validation - Built-in rules
  4. Field-level access - Filter writable fields
  5. beforeOperation - Pre-operation side effects
  6. Database operation
  7. afterOperation - Post-operation side effects

Context API

Creating Context

import { getContext } from '@opensaas/stack-core'

// With session
const context = await getContext(config, prisma, { userId: '123' })

// Anonymous
const context = await getContext(config, prisma, null)

Using Context

// All Prisma operations supported
const post = await context.db.post.create({ data })
const posts = await context.db.post.findMany()
const post = await context.db.post.findUnique({ where: { id } })
const post = await context.db.post.update({ where: { id }, data })
const post = await context.db.post.delete({ where: { id } })

// Access control is automatic
// Returns null/[] if access denied

Generators

Prisma Schema

import { writePrismaSchema } from '@opensaas/stack-core'

writePrismaSchema(config, './prisma/schema.prisma')

TypeScript Types

import { writeTypes } from '@opensaas/stack-core'

writeTypes(config, './.opensaas/types.ts')

Utility Functions

import { getDbKey, getUrlKey, getListKeyFromUrl } from '@opensaas/stack-core'

getDbKey('BlogPost') // 'blogPost' - for context.db access
getUrlKey('BlogPost') // 'blog-post' - for URLs
getListKeyFromUrl('blog-post') // 'BlogPost' - parse from URLs

Validation

Built-in validation with Zod:

text({
  validation: {
    isRequired: true,
    length: { min: 3, max: 100 },
  },
})

integer({
  validation: {
    isRequired: true,
    min: 0,
    max: 1000,
  },
})

Custom validation in hooks:

hooks: {
  validateInput: async ({ operation, resolvedData }) => {
    if (operation === 'delete') return
    const { title } = resolvedData
    if (title && !isValidSlug(slugify(title))) {
      throw new ValidationError('Title contains invalid characters')
    }
  }
}

Testing

import { describe, it, expect } from 'vitest'
import { getContext } from '@opensaas/stack-core'
import config from './opensaas.config'

describe('Post access control', () => {
  it('allows author to update their post', async () => {
    const context = await getContext(config, prisma, { userId: authorId })
    const updated = await context.db.post.update({
      where: { id: postId },
      data: { title: 'New Title' },
    })
    expect(updated).toBeTruthy()
    expect(updated?.title).toBe('New Title')
  })

  it('denies non-author from updating post', async () => {
    const context = await getContext(config, prisma, { userId: otherUserId })
    const updated = await context.db.post.update({
      where: { id: postId },
      data: { title: 'Hacked!' },
    })
    expect(updated).toBeNull() // Silent failure
  })
})

Examples

Learn More

License

MIT