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 🙏

© 2024 – Pkg Stats / Ryan Hefner

zod-to-dynamodb-onetable-schema

v1.0.0

Published

Auto-generate `dynamodb-onetable` model schemas using `zod`, with best-in-class autocomplete

Downloads

496

Readme

💍 zod-to-dynamodb-onetable-schema

Auto-generate dynamodb-onetable model schemas using zod, with best-in-class autocomplete

Overview

  • Convert zod objects into dynamo-onetable model schemas
  • Convert zod schemas into dynamo-onetable model field schemas
  • Get dynamic autocomplete as you expect from dynamo-onetable via type-fu 🥋
  • Un-representable data-types cause errors, un-representable checks will notify you via logger.debug if you provide a Winston instance
  • Zero dependencies - compatible with zod@^3.23.8 and dynamo-onetable@^2.7.5

Rationale

dyanmodb-onetable provides a fantastic API for building and interacting with DynamoDB single-table designs. In using it, I've come to appreciate a couple of areas where I wanted something slightly different:

  1. The validation option offers a single regex pattern per field (and we all know how regex goes)
  2. Defining the schema can be tricky because using the supplied types clobbers the library's ability to infer your specific models

Enter, zod, which excels at providing a flexible schema-building API and parsing data. This library aims to bridge the two, giving you all the benefits of dynamodb-onetable while delegating model schema building and parsing to zod, which has proven itself as a capable library for those jobs.

Install

npm i zod-to-dynamodb-onetable-schema

Quick start

Say you have an existing 'Account' schema using zod in your application code:

import { z } from "zod";

const accountSchema = z.object({
  id: z.string().uuid(),
  email: z.string(),
  status: z.enum(["verified", "unverified"]),
});

Defining a Account model is now easy. We'll extend it to include our table's indexes and pass it to zodOneModelSchema.

import { zodOneModelSchema } from "zod-to-dynamodb-onetable-schema";
import { Table } from "dynamodb-onetable";

const accountRecordSchema = accountSchema.extend({
  pk: z.literal("${_type}#${id}"), // 👈 more about this later
  sk: z.literal("${_type}#"),
});

const table = new Table({
  // other fields collapsed,
  schema: {
    indexes: { primary: { hash: "pk", sort: "sk" } },
    models: { Account: zodOneModelSchema(accountRecordSchema) },
  },
});

We can now use our new Account model...

const accountModel = table.getModel("Account");

const newAccount: z.infer<typeof accountSchema> = {
  id: uuidv4(),
  email: "[email protected]",
  status: "unverified",
};

await accountModel.create(newAccount);
const storedAccount = await accountModel.get(newAccount);
expect(newAccount).toEqual(storedAccount);

Notice we didn't need to specify the pk or pk? That's because Table handles it for us when we use z.literal() with OneTable's value template syntax. The typing is smart enough to identify that these values can be automatically extracted from your entity data and aren't needed.

A deeper dive

Explicitly setting indexes

If you don't want to use z.literal() and OneTable's value template syntax, you can set your indexes using z.string() and z.number() as you would expect.

import { Table } from "dynamodb-onetable";
import { zodOneModelSchema } from "zod-to-dynamodb-onetable-schema";
import { z } from "zod";

const accountRecordSchema = z.object({
  pk: z.string(),
  sk: z.string(),
  id: z.string().uuid(),
  email: z.string(),
  status: z.enum(["verified", "unverified"]),
});

const table = new Table({
  // other fields collapsed,
  schema: {
    indexes: { primary: { hash: "pk", sort: "sk" } },
    models: { Account: zodOneModelSchema(accountRecordSchema) },
  },
});

const accountModel = table.getModel("Account");

const newAccount: z.infer<typeof accountRecordSchema> = {
  pk: "Account#1",
  sk: "Account",
  id: "1",
  email: "[email protected]",
  status: "unverified",
};

await accountModel.create(newAccount);
const storedAccount = await accountModel.get(newAccount);
expect(newAccount).toMatchObject(storedAccount);

Mixing OneTable schema syntax with zod schemas

This library also supports partial zod schema definition via the zodOneFieldSchema export. In this example, we add a complex schema using the zod API to a nested attribute.

import { Table } from "dynamodb-onetable";
import { zodOneFieldSchema } from "zod-to-dynamodb-onetable-schema";

const table = new Table({
  // other fields collapsed,
  schema: {
    indexes: { primary: { hash: "pk", sort: "sk" } },
    models: {
      Account: {
        pk: { type: String, required: true },
        sk: { type: String, required: true },
        account: {
          type: "object",
          required: true,
          schema: {
            id: { type: String, required: true },
            //     👇  utilize our zod converter
            emails: zodOneFieldSchema(
              z.array(
                z.object({
                  email: z.string().email(),
                  isVerified: z.boolean(),
                }),
              ),
            ),
          },
        },
      },
    },
  },
});

Thanks to the type-fu 🥋 of ZodToOneField, even nesting our converter like this will still leave you with best-in-class autocomplete in the Table instance.

Decoupling the schema from Table

You might get to a point where you want to have multiple Table instances, at which point you'll want to have one source of truth for your schema. Likewise, you might want to inject your Table while still getting full autocomplete.

In short, the answer is to use Table<typeof oneTableSchema> as your injectable table where oneTableSchema satisfies OneSchema!

import { OneSchema, Table } from "dynamodb-onetable";
import { z } from "zod";
import { zodOneModelSchema } from "../src";

const accountSchema = z.object({
  id: z.string().uuid(),
  email: z.string(),
  status: z.enum(["verified", "unverified"]),
});

type Account = z.infer<typeof accountSchema>;

interface AccountStore {
  getAccount: (accountId: string) => Promise<Account | null>;
}

const accountRecordSchema = accountSchema.extend({
  pk: z.literal("${_type}#${id}"),
  sk: z.literal("${_type}#"),
});

const oneTableSchema = {
  // other attributes collapsed
  indexes: { primary: { hash: "pk", sort: "sk" } },
  models: { Account: zodOneModelSchema(accountRecordSchema) },
} satisfies OneSchema;

class AccountOneTableStore implements AccountStore {
  constructor(private readonly table: Table<typeof oneTableSchema>) {}

  async getAccount(accountId: string): Promise<Account | null> {
    try {
      const data = await this.table.getModel("Account").get({ id: accountId });
      return accountSchema.parse(data);
    } catch (err) {
      console.info("Account could not be found in OneTable", { err });
      return null;
    }
  }
}

const table = new Table({
  // other attributes collapsed
  schema: oneTableSchema,
});

const accountStore = new AccountOneTableStore(table);

const account = await accountStore.get("test-id");

Contributing

I appreciate any contributions, issues or discussions. My aim is to make contributing quick and easy.

Please note that PR quality checks enforce a 100% code coverage rate and will test your code against a local version of DynamoDB. Passing these requirements are essential to getting a merge/release. For new code, at least some tests should interface with an instance of Table that interacts with a local DynamoDB instance. An example of this test type is at tests/zodOneModelSchema.spec.ts.

Here's a quick start to getting this repo running on your own machine (assumes you already have gh, node, pnpm and docker installed):

  1. Clone the repo to your own machine
gh repo clone jharlow/zod-to-dynamodb-onetable-schema
  1. Start an instance of dynamodb-local on your machine
docker run -d -p 8000:8000 amazon/dynamodb-local
  1. Install dependencies
pnpm install
  1. You can now execute the test suite and develop 🙌
pnpm test
  1. Before pushing, check your work will pass checks:
pnpm pr-checks