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

@qninhdt/typespec-orm

v0.5.0

Published

TypeSpec decorator library for defining database ORM schemas - @table, @key, @index, @foreignKey and more.

Downloads

572

Readme

@qninhdt/typespec-orm

Shared TypeSpec ORM library used by all emitters in this repository.

This package is where the schema contract actually lives. It defines decorators, validation behavior, relation resolution, namespace normalization, selector filtering, and the normalized ORM graph consumed by the emitters.

What This Package Owns

@qninhdt/typespec-orm is responsible for:

  • declaring the public decorators used in TypeSpec
  • validating ORM-managed declarations
  • resolving relation ownership and foreign keys
  • expanding @tableMixin
  • building the normalized namespace-aware model graph
  • applying shared include / exclude selection logic
  • surfacing shared diagnostics before emitters write files

It does not emit code by itself.

Installation

pnpm add -D @typespec/compiler @qninhdt/typespec-orm

Importing The Library

import "@qninhdt/typespec-orm";

using Qninhdt.Orm;

Core Concepts

Namespaces are required

@table, @data, and @tableMixin must be declared inside a namespace. Required referenced declarations must also be namespaced.

This matters because the shared ORM graph treats namespaces as the source of truth for:

  • output paths
  • package structure
  • import calculation
  • selector filtering
  • dependency validation

@tableMixin

@tableMixin exists for reusable persisted field groups.

Mixins:

  • are validated
  • can inherit from other mixins
  • can be composed into tables
  • are never emitted as standalone tables

Field collisions between mixins or between a mixin and a child model are errors.

Shared normalization

Emitters do not re-discover tables independently anymore. They consume the normalized graph produced by the ORM core, which includes:

  • kind: table, data, or mixin
  • namespace and namespace path
  • namespace-derived output directory
  • dependencies on models, mixins, enums, and scalars
  • selected models after filtering

Normalized Graph Contract

The normalized graph is the contract between this package and the emitters. Each normalized model includes:

  • its ORM kind: table, data, or mixin
  • full dotted declaration name
  • namespace segments and a snake_case namespace path
  • namespace-derived output directory and leaf package name
  • resolved mixin sources
  • hard and soft dependencies on models, mixins, enums, and scalars

That shared graph is what makes the emitters behave consistently. GORM, SQLModel, Zod, and DBML no longer perform their own disconnected model discovery passes.

Namespace Normalization Rules

Namespace handling is intentionally deterministic:

  • namespace segments are preserved for selection and diagnostics
  • output paths convert each segment with camelToSnake
  • package or module leaf names come from the final normalized namespace segment
  • root-level declarations are rejected instead of being assigned an implicit folder

Example:

namespace Demo.GamePlatform.Content.Stories;

normalizes to:

  • namespace: Demo.GamePlatform.Content.Stories
  • namespace path: ["demo", "game_platform", "content", "stories"]
  • namespace directory: demo/game_platform/content/stories
  • package leaf: stories

Decorator Reference

Model decorators

  • @table(name?) Marks a model as a persisted table. If name is omitted, the table name is derived from the model name.

  • @tableMixin Marks a model as a reusable ORM mixin.

  • @data(label?) Marks a model as a non-table data shape for forms and DTOs.

Column and persistence decorators

  • @map(columnName) Overrides the emitted column name.

  • @index(name?) Adds a non-unique index.

  • @unique Adds a unique constraint for the field.

  • @check(name, expression) Adds a named database check constraint anchored to the property.

  • @precision(precision, scale?) Adds numeric precision metadata.

  • @autoIncrement Marks an integer field as auto-incrementing.

  • @softDelete Marks a datetime field as the soft-delete column.

  • @autoCreateTime Marks a datetime field as create timestamp metadata.

  • @autoUpdateTime Marks a datetime field as update timestamp metadata.

  • @ignore Removes a property from persistence emitters.

Relation decorators

  • @foreignKey(localField, referencedField?) Declares the owning side of a relation using a local field and an optional referenced target field.

  • @mappedBy(inverseProperty) Declares the inverse side of a relation.

  • @manyToMany(joinTableName) Declares many-to-many shorthand on an array relation.

  • @onDelete(action) Declares FK delete behavior.

  • @onUpdate(action) Declares FK update behavior.

Form metadata decorators

  • @title(text)
  • @placeholder(text)
  • @@inputType(scalar, htmlType)

@@inputType targets a scalar. When applied to a field, use Field::type or the source scalar for lookup-typed fields:

@@inputType(CreateWorldForm.summary::type, "textarea");
@@inputType(Demo.Worlds.World.prompt::type, "textarea");

Basic Example

import "@qninhdt/typespec-orm";

using Qninhdt.Orm;

namespace Demo.Shared;

@tableMixin
model Timestamped {
  @key id: uuid;
  @autoCreateTime createdAt: utcDateTime;
  @autoUpdateTime updatedAt?: utcDateTime;
}

namespace Demo.Accounts;

@table
model User is Demo.Shared.Timestamped {
  @unique
  @maxLength(320)
  @format("email")
  email: string;

  @check("users_credits_non_negative", "credits >= 0")
  credits: int32 = 0;

  @manyToMany("user_badges")
  badges?: Badge[];
}

@table
model Badge is Demo.Shared.Timestamped {
  @unique code: string;

  @manyToMany("user_badges")
  users?: User[];
}

namespace Demo.Worlds;

@table
model World is Demo.Shared.Timestamped {
  ownerId: uuid;
  slug: string;

  @foreignKey("ownerId")
  owner: Demo.Accounts.User;
}

namespace Demo.Forms;

@data("Create Invitation Form")
model CreateInvitationForm {
  @title("Invitee Email")
  @placeholder("[email protected]")
  inviteeEmail: Demo.Accounts.User.email;
}

Relation Semantics

Owned relations

Owned relations are declared on the navigation property:

authorId: uuid;

@foreignKey("authorId")
author: User;

The optional second argument targets a non-id field:

organizationCode: string;

@foreignKey("organizationCode", "code")
organization: Organization;

Inverse relations

@mappedBy("author")
posts: Post[];

Many-to-many shorthand

@table
model User {
  @key id: uuid;

  @manyToMany("user_badges")
  badges?: Badge[];
}

@table
model Badge {
  @key id: uuid;

  @manyToMany("user_badges")
  users?: User[];
}

Rules:

  • the property must be an array of @table models
  • both sides must declare @manyToMany
  • both sides must use the same join table name
  • shorthand conflicts with an explicit table of the same name
  • shorthand is for simple joins only; payload-column join tables should be modeled explicitly

Lookup Types And Property Reuse

This package supports source-property reuse patterns such as:

@data
model PublicUser {
  email: Demo.GamePlatform.Accounts.User.email;
}

That lets @data models and other consumers inherit the source property's underlying scalar type and constraints without duplicating the full column definition manually.

Use lookup types when you want:

  • one source of truth for field-level constraints
  • consistent frontend and backend validation
  • shared field descriptions across namespaces

Avoid lookup types when the derived model needs materially different semantics; define a dedicated property instead.

Shared Selector Model

Emitters using this core support:

include:
  - "Demo.Worlds"
  - "Demo.Forms"
exclude:
  - "Demo.Audit"

Selectors can match:

  • a namespace subtree
  • a concrete declaration

There is no wildcard syntax. Selectors are matched by dotted-name prefix semantics.

Examples:

| Selector | Matches | | -------------------------------------- | ------------------------------------------------------------------ | | Demo.GamePlatform.Forms | the entire forms subtree | | Demo.GamePlatform.Accounts.User | the User declaration specifically | | Demo.GamePlatform.Content | all content namespaces below it | | Demo.GamePlatform.Audit in exclude | removes the audit subtree even if Demo.GamePlatform was included |

Behavior:

  • exclude wins over include
  • redundant selectors warn
  • selecting a model while excluding a required dependency fails

Modeling Checklist

Before handing a schema to an emitter, it helps to check these rules:

  • every emitted or referenced ORM-managed declaration has a namespace
  • shared persisted fragments use @tableMixin
  • ownership is explicit on foreign-key relations
  • many-to-many shorthand is used only for simple join tables
  • selector filters still include every required dependency
  • names are stable enough for namespace-derived package paths

Diagnostics

Important diagnostics surfaced by the core include:

  • namespace-required
  • duplicate-table-name
  • duplicate-column-name
  • mixin-cycle
  • mixin-field-conflict
  • filtered-dependency
  • unsupported-relation-shape
  • foreign-key-local-missing
  • foreign-key-target-missing
  • foreign-key-type-mismatch
  • one-to-one-missing-unique
  • many-to-many-not-array
  • many-to-many-target-not-table
  • many-to-many-missing-inverse
  • many-to-many-conflicting-table
  • many-to-many-conflicting-explicit-table
  • duplicate-constraint-name

Troubleshooting Common Diagnostics

namespace-required

The model, mixin, or required dependency lives in the global namespace. Move it under a namespace and recompile.

filtered-dependency

Your emitter selection rules included a model but excluded one of its dependencies. Either widen the include set or stop excluding the dependency.

mixin-cycle

Two or more mixins inherit from each other in a loop. Break the cycle by extracting the shared fields into a separate base mixin.

mixin-field-conflict

Two mixins, or a mixin plus a child model, define the same field name. Phase 1+ intentionally treats this as an error instead of allowing silent override behavior.

foreign-key-type-mismatch

The local field type does not line up with the referenced target field type. Make both sides use compatible scalars.

many-to-many-missing-inverse

Both sides of a shorthand many-to-many relation must opt in. Add the inverse relation with the same join-table name.

Guidance For Emitter Authors

If you are adding or maintaining an emitter in this repo:

  • consume the normalized ORM graph instead of walking raw compiler state ad hoc
  • treat namespace-derived output paths as canonical
  • reuse shared selector behavior
  • rely on shared relation resolution where possible
  • do not silently downgrade unsupported persistence mappings

Limitations And Boundaries

  • root-level emitted models are unsupported
  • namespace-less dependencies are errors
  • many-to-many shorthand does not support payload columns
  • mixin field collisions are errors rather than override points

Made with heart by @qninhdt, with GPT-5.4 and Claude Opus 4.6.