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

typespec-electrodb-emitter

v5.0.0

Published

Generate [ElectroDB](https://electrodb.dev) entities from your TypeSpec models.

Readme

typespec-electrodb-emitter

Generate ElectroDB entities from your TypeSpec models.

Install

npm i -D typespec-electrodb-emitter

Usage

Add the emitter to your tspconfig.yaml:

emit:
  - typespec-electrodb-emitter
options:
  typespec-electrodb-emitter:
    package-name: "@mycorp/ddb-entities"
    package-version: "0.1.0"
    emitter-output-dir: "{cwd}/build/entities"

Import the library in your TypeSpec, annotate a model, and compile:

import "typespec-electrodb-emitter";

@entity("task", "org")
@index("tasks", { pk: [Task.id] })
model Task {
    id: string;
    title: string;
}
tsp compile .

The emitter writes a ready-to-publish package (index.mjs/.cjs/.d.ts plus package.json) to emitter-output-dir.

| Option | Default | Description | | --- | --- | --- | | package-name | entities | Name of the generated package. | | package-version | 1.0.0 | Version of the generated package. | | emitter-output-dir | emitter default | Where the package is written. | | model-base | off | Opt-in base-class emission (see below). |

Decorators

Annotate models with @entity and @index, and properties with the rest.

| Decorator | Target | Purpose | | --- | --- | --- | | @entity(name, service, version?) | model | Marks a model as an ElectroDB entity. | | @index(name, accessPattern) | model | Defines a primary or secondary index. | | @label(label) | property | Sets the ElectroDB attribute label. | | @createdAt(label?) | property | Managed created-at timestamp. | | @updatedAt(label?) | property | Managed updated-at timestamp. | | @semanticVersion | property | Stores a semver sort key that sorts correctly. |

@index takes a shorthand ({ pk: [Model.id] }) or a full access pattern with index, collection, scope, pk, and sk. See tsp/main.tsp for the full signatures.

@semanticVersion

DynamoDB compares sort keys byte-lexicographically, so a plain semver string sorts wrong once a segment reaches two digits ("1.10.0" < "1.9.0"). @semanticVersion zero-pads each segment on write and reverses it on read, so callers still see "1.10.0" while the key sorts in true version order. "Give me the latest version" becomes a native ScanIndexForward:false, Limit:1 query with no marker row to maintain.

The property must be typed as the exported SemanticVersion scalar. Each segment is capped at six digits (0–999999); a wider type is a compile-time error.

@entity("productRelease", "catalog")
@index("releases", {
    pk: [ProductRelease.productCode],
    sk: [ProductRelease.version],
})
model ProductRelease {
    productCode: string;

    @semanticVersion
    version: SemanticVersion;
}
// Highest version for a product, no marker entity, no app-level compare:
await ProductReleaseEntity.query
    .releases({ productCode: "widget" })
    .go({ order: "desc", limit: 1 });

Reading by version

ElectroDB applies attribute setters on writes only. A version handed to get, query, patch, update, or delete reaches key composition unencoded and addresses a row that was never written, so ProductReleaseEntity.get({ productCode: "widget", version: "1.10.0" }) returns nothing. Encode the version before it reaches the entity.

With model-base, the generated class does it:

await ProductReleaseEntity.get(
    ProductReleaseModelBase.prepareQuery({ productCode: "widget", version: "1.10.0" }),
).go();

Without it, call the attribute's own setter, so the padding is never restated:

const version = ProductRelease.attributes.version.set("1.10.0");
await ProductReleaseEntity.get({ productCode: "widget", version }).go();

Model base classes (opt-in)

Set the model-base option to also emit one generation-gap base class per @entity, wired to a runtime base class you supply:

options:
  typespec-electrodb-emitter:
    model-base:
      module: "@example/electrodb-base"
      class-name: "BaseModel"
      config-type: "BaseModelConfig"

Each entity gets its own file (pet-model-base.mjs and friends) with its own exports subpath — no barrel, so importing one entity never pulls in another. Your business logic lives in a subclass and survives regeneration:

import { PetModelBase } from "@mycorp/ddb-entities/pet-model-base";

export class PetModel extends PetModelBase {
    adopt(petId: string) {
        /* business logic */
    }
}

Your class-name must take one type parameter (instantiated as <typeof Entity>) and a two-argument constructor (schema, config), where config-type names the config type. The generated class forwards its own schema to super(schema, config).

Every generated class carries a static prepareQuery, which encodes any @semanticVersion attributes in a set of key facets and leaves the rest alone (see Reading by version). It is present on every entity, decorated or not, so adding @semanticVersion later starts working without touching call sites.

The emitter generates only this wiring; the base class must provide the read-after-write safety it relies on (update/upsert/patch with response: "all_new", no read-back after a write). ElectroDB transactions only support all_old, so post-transaction reads keep DynamoDB's eventual-consistency race.

Leaving model-base unset keeps the output byte-identical to before the option existed.

Full example

test/main.tsp exercises the whole surface in one file: nested maps and lists, enums (string-, numeric-, and member-valued), managed timestamps, @label renames, semantic-version sort keys, empty-set handling, multiple indexes on one entity (isolated, clustered, and scoped GSIs), and opt-in model bases. Copy it into a project, run tsp compile, and read the generated index.d.ts to see exactly what each annotation produces.

@entity("person", "org")
@index("persons", { pk: [Person.pk] })
@index("jobs", {
    collection: "jobs",
    index: "gsi1",
    pk: [Person.personId],
    sk: [Person.firstName],
})
@index("byAge", { index: "lsi1", pk: [Person.pk], sk: [Person.age] })
model Person {
    @invisible(Lifecycle) pk: UUID;
    personId: UUID;

    @label("fn") firstName: String64;

    @createdAt createdAt: int32;
    @updatedAt updatedAt: int32;
    age: int16;
    address: Address;
    contact: Contact[];
    nickName?: string;
}

Reference

See tsp/main.tsp for the type definitions of every annotation.