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

@datrix/core

v0.2.0

Published

Core functionality for Datrix framework - schema, validation, query building, migration

Readme

@datrix/core

The heart of Datrix. Handles schema definition, validation, query building, relation processing, and database migrations — without being tied to a specific database engine.

Installation

pnpm add @datrix/core

Setup

Two functions are the entry point to every Datrix project: defineSchema and defineConfig.

defineSchema

Validates your schema object against SchemaDefinition at the TypeScript level. At runtime it returns the object as-is — it is a type helper, not a factory.

import { defineSchema } from "@datrix/core"

export const postSchema = defineSchema({
  name: "post",
  fields: {
    title:   { type: "string", required: true },
    status:  { type: "enum", values: ["draft", "published"] as const, default: "draft" },
    author:  { type: "relation", kind: "belongsTo", model: "user" },
  },
  indexes: [
    { fields: ["status"] },
  ],
})

defineConfig

Creates the Datrix instance factory. Calling the returned function initializes the instance once — subsequent calls return the cached instance.

import { defineConfig } from "@datrix/core"
import { PostgresAdapter } from "@datrix/adapter-postgres"
import { postSchema, userSchema } from "./schemas"

const getDatrix = defineConfig(() => ({
  adapter: new PostgresAdapter({
    host:     "localhost",
    port:     5432,
    database: "mydb",
    user:     "postgres",
    password: process.env.DB_PASSWORD,
  }),
  schemas: [userSchema, postSchema],
  plugins: [],
  migration: {
    auto:      false,
    directory: "./migrations",
  },
  dev: {
    logging: false,
  },
}))

export default getDatrix

CRUD

const datrix = await getDatrix()

// Read
const posts    = await datrix.findMany("post", { where: { status: "published" }, limit: 10 })
const post     = await datrix.findOne("post", { slug: "hello-world" })
const byId     = await datrix.findById("post", 1)
const total    = await datrix.count("post", { status: "draft" })

// Write
const created  = await datrix.create("post", { title: "Hello", status: "draft", author: 1 })
const updated  = await datrix.update("post", created.id, { status: "published" })
const deleted  = await datrix.delete("post", created.id)

// Bulk
const many     = await datrix.createMany("post", [{ title: "A" }, { title: "B" }])
const updated2 = await datrix.updateMany("post", { status: "draft" }, { status: "archived" })
const deleted2 = await datrix.deleteMany("post", { status: "archived" })

Every record automatically includes id, createdAt, and updatedAt — these cannot be defined manually.

raw

Use datrix.raw to bypass plugin hooks (onBeforeQuery / onAfterQuery). Method signatures are identical — the only difference is that schema lifecycle hooks and plugin hooks do not fire.

await datrix.raw.create("post", { title: "Silent insert" })

Field types

| Type | Key options | | ---------- | --------------------------------------------------------------------- | | string | required, minLength, maxLength, unique, pattern, default, validator | | number | required, min, max, integer, unique, default, validator | | boolean | required, default | | date | required, min, max, default | | enum | values (required), default | | array | items (field def), minItems, maxItems, unique | | json | required, default | | file | allowedTypes, maxSize, multiple | | relation | kind, model, foreignKey, through, onDelete, onUpdate |

Relations

Datrix manages foreign keys and junction tables automatically — you never define them manually.

| Kind | FK location | Use when | | ------------- | ----------------- | ------------------------------------ | | belongsTo | Current schema | This record owns the FK (N:1) | | hasOne | Target schema | Target owns the FK (1:1) | | hasMany | Target schema | Target owns the FK (1:N) | | manyToMany | Junction table | Junction auto-created unless through is set |

// belongsTo — adds `authorId` to posts table
author: { type: "relation", kind: "belongsTo", model: "user", onDelete: "restrict" }

// hasMany — adds `postId` to comments table
comments: { type: "relation", kind: "hasMany", model: "comment" }

// manyToMany — creates post_tag junction table
tags: { type: "relation", kind: "manyToMany", model: "tag" }

Querying

Filtering

// Comparison operators
await datrix.findMany("user", {
  where: {
    age:   { $gte: 18, $lte: 65 },
    email: { $like: "%@example.com" },
    role:  { $in: ["admin", "editor"] },
  },
})

// Logical operators
await datrix.findMany("post", {
  where: { $or: [{ status: "published" }, { featured: true }] },
})

// Nested relation filter
await datrix.findMany("post", {
  where: { author: { verified: true } },
})

All comparison operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like, $ilike, $startsWith, $endsWith, $contains, $null, $notNull, $exists.

Populate

// All relations
await datrix.findMany("post", { populate: "*" })

// Specific relations with options
await datrix.findMany("post", {
  populate: {
    author:   { select: ["id", "name"] },
    comments: { where: { approved: true }, limit: 5, orderBy: { createdAt: "desc" } },
  },
})

Sorting and pagination

await datrix.findMany("post", {
  orderBy: [{ field: "createdAt", direction: "desc", nulls: "last" }],
  limit:   20,
  offset:  40,
})

Lifecycle hooks

Hooks run on every non-raw query. They fire after plugin hooks. Every before hook must return the (optionally modified) value.

ctx.metadata is a mutable object shared between the before and after hook of the same operation.

defineSchema({
  name: "post",
  fields: { ... },
  hooks: {
    beforeCreate: async (data, ctx) => {
      return { ...data, slug: data.title?.toLowerCase().replace(/ /g, "-") }
    },
    afterCreate: async (record, ctx) => {
      return record
    },
    beforeFind: async (query, ctx) => {
      return { ...query, where: { ...query.where, status: "published" } }
    },
    afterFind: async (results, ctx) => {
      return results
    },
    beforeDelete: async (id, ctx) => id,
    afterDelete:  async (id, ctx) => {},
  },
})

Permissions

Schema and field permissions are defined in the schema and enforced by @datrix/api. The core package carries the config — enforcement is in the API layer.

defineSchema({
  name: "post",
  permission: {
    create: ["admin", "editor"],
    read:   true,
    update: (ctx) => ctx.user?.id === ctx.record?.authorId,
    delete: ["admin"],
  },
  fields: {
    email: {
      type: "string",
      permission: {
        read:  ["admin"],  // stripped from response for other roles
        write: ["admin"],  // 403 for other roles on create/update
      },
    },
  },
})

Migration

Migrations are managed through the Datrix CLI. The core package exposes beginMigrate() which the CLI uses internally.

datrix migrate   # diff schemas against DB, prompt for ambiguous changes, apply

Direct API use is possible but not the intended workflow:

const session = await datrix.beginMigrate()

if (session.hasAmbiguous) {
  // Resolve rename vs. drop+add for each ambiguous change
  session.resolveAmbiguous("user.name->lastname", "rename")
}

await session.apply()

Architecture

src/
├── index.ts              # Public exports (defineSchema, defineConfig)
├── datrix.ts              # Datrix class — instance factory, CRUD dispatcher
├── schema-registry.ts    # SchemaRegistry — schema storage and lookup
├── initializer.ts        # Startup sequence — schema finalization, plugin init
├── mixins/
│   └── crud.ts           # CRUD method implementations
├── query/
│   ├── builder.ts        # QueryBuilder — constructs QueryObject from options
│   └── executor.ts       # QueryExecutor — routes QueryObject to the adapter
├── validation/
│   └── validator.ts      # Field-level validation before writes
├── migration/
│   ├── migrator.ts       # MigrationSession — diff and apply logic
│   └── planner.ts        # Change detection and plan generation
└── plugin/
    └── plugin.ts         # BasePlugin — base class for all Datrix plugins