This package has been deprecated

Author message:

Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

@sanity-typed/schema-builder
TypeScript icon, indicating that this package has built-in type declarations

3.0.1 • Public • Published

@sanity-typed/schema-builder [DEPRECATED]

🛑🛑🛑 This has been deprecated for @sanity-typed/types

It's approach is as close as possible to sanity's native schema definitions and will solve this problem in a more maintainable way. With the introduction of the defineType, defineField, and defineArrayMember methods, direct inference of typed values is possible.

Build Sanity schemas declaratively and get typescript types of schema values for free!

Install

npm install @sanity-typed/schema-builder sanity

Usage

import { s } from "@sanity-typed/schema-builder";

const foo = s.document({
  name: "foo",
  fields: [
    {
      name: "foo",
      type: s.string(),
    },
    {
      name: "bar",
      type: s.array({
        of: [s.datetime(), s.number({ readOnly: true })],
      }),
    },
    {
      name: "hello",
      optional: true,
      type: s.object({
        fields: [
          {
            name: "world",
            type: s.number(),
          },
        ],
      }),
    },
  ],
});

// Use schemas in Sanity https://www.sanity.io/docs/schema-types
export default defineConfig({
  schema: {
    types: [foo.schema()],
  },
});

Your sanity client's return values can be typed with s.infer:

import { createClient } from "@sanity/client";

const client = createClient(/* ... */);

// results are automatically typed from the schema!
const result: s.infer<typeof foo> = await client.fetch(`* [_type == "foo"][0]`);

/**
 *  typeof result === {
 *    _createdAt: string;
 *    _id: string;
 *    _rev: string;
 *    _type: "foo";
 *    _updatedAt: string;
 *    bar: (string | number)[];
 *    foo: string;
 *    hello?: {
 *      world: number;
 *    };
 *  };
 **/

Because sanity returns JSON values, some values require conversion (ie changing most date strings into Dates). This is available with .parse:

const parsedValue: s.output<typeof foo> = foo.parse(result);

/**
 *  typeof parsedValue === {
 *    _createdAt: Date;
 *    _id: string;
 *    _rev: string;
 *    _type: "foo";
 *    _updatedAt: Date;
 *    bar: (Date | number)[];
 *    foo: string;
 *    hello?: {
 *      world: number;
 *    };
 *  };
 **/

Mocks that match your schema can be generated with .mock:

// Use @faker-js/faker to create mocks for tests!
import { faker } from "@faker-js/faker";

const mock = foo.mock(faker);

/**
 *  Same type as s.infer<typeof foo>
 *
 *  typeof mock === {
 *    _createdAt: string;
 *    _id: string;
 *    _rev: string;
 *    _type: "foo";
 *    _updatedAt: string;
 *    bar: (string | number)[];
 *    foo: string;
 *    hello?: {
 *      world: number;
 *    };
 *  };
 **/

Types

All methods correspond to a Schema Type and pass through their corresponding Schema Type Properties as-is. For example, s.string(def) takes the usual properties of the sanity string type. Sanity's types documentation should "just work" with these types.

The notable difference is between how the sanity schema, the type property, and the name / title / description property are defined. The differentiator is that the s.* methods replace type, not the entire field:

// This is how schemas are defined in sanity
const schema = {
  type: "document",
  name: "foo",
  fields: [
    {
      name: "bar",
      title: "Bar",
      description: "The Bar",
      type: "string",
    },
  ],
};

// This is the corresponding type in @sanity-typed/schema-builder
const type = s.document({
  name: "foo",
  fields: [
    {
      name: "bar",
      title: "Bar",
      description: "The Bar",
      type: s.string(),
    },
  ],
});

// INVALID!!!
const invalidType = s.document({
  name: "foo",
  fields: [
    // This is invalid. s.string is a type, not an entire field.
    s.string({
      name: "bar",
      title: "Bar",
      description: "The Bar",
    }),
  ],
});

The only types with names directly in the type are s.document (because all documents are named and not nested) and s.objectNamed (because named objects have unique behavior from nameless objects).

Types with Fields

For types with fields (ie s.document, s.object, s.objectNamed, s.file, and s.image) all fields are required by default (rather than sanity's default, which is optional by default). You can set it to optional: true.

const type = s.object({
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   foo: number;
 *   bar?: number;
 * }
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   foo: number;
 *   bar?: number;
 * }
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "object",
 *   fields: [
 *     {
 *       name: "foo",
 *       type: "number",
 *       validation: (Rule) => Rule.validation(),
 *     },
 *     {
 *       name: "bar",
 *       type: "number",
 *     },
 *   ],
 * };
 */

Array

All array type properties pass through with the exceptions noted in Types.

Other exceptions include min, max, and length. These values are used in the zod validations, the sanity validations, and the inferred types.

const type = s.array({
  of: [s.boolean(), s.datetime()],
});

type Value = s.infer<typeof type>;

/**
 * type Value === (boolean | string)[];
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === (boolean | Date)[];
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "array",
 *   of: [{ type: "boolean" }, { type: "datetime" }],
 *   ...
 * };
 */
const type = s.array({
  min: 1,
  of: [s.boolean()],
});

type Value = s.infer<typeof type>;

/**
 * type Value === [boolean, ...boolean[]];
 */
const type = s.array({
  max: 2,
  of: [s.boolean()],
});

type Value = s.infer<typeof type>;

/**
 * type Value === [] | [boolean] | [boolean, boolean];
 */
const type = s.array({
  min: 1,
  max: 2,
  of: [s.boolean()],
});

type Value = s.infer<typeof type>;

/**
 * type Value === [boolean] | [boolean, boolean];
 */
const type = s.array({
  length: 3,
  of: [s.boolean()],
});

type Value = s.infer<typeof type>;

/**
 * type Value === [boolean, boolean, boolean];
 */

Block

All block type properties pass through with the exceptions noted in Types.

const type = s.block();

type Value = s.infer<typeof type>;

/**
 * type Value === PortableTextBlock;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === PortableTextBlock;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "block",
 *   ...
 * };
 */

Boolean

All boolean type properties pass through with the exceptions noted in Types.

const type = s.boolean();

type Value = s.infer<typeof type>;

/**
 * type Value === boolean;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === boolean;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "boolean",
 *   ...
 * };
 */

Date

All date type properties pass through with the exceptions noted in Types.

const type = s.date();

type Value = s.infer<typeof type>;

/**
 * type Value === string;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "date",
 *   ...
 * };
 */

Datetime

All datetime type properties pass through with the exceptions noted in Types.

Other exceptions include min and max. These values are used in the zod validations and the sanity validations.

Datetime parses into a javascript Date.

const type = s.datetime();

type Value = s.infer<typeof type>;

/**
 * type Value === string;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === Date;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "datetime",
 *   ...
 * };
 */

Document

All document type properties pass through with the exceptions noted in Types and Types with Fields.

const type = s.document({
  name: "foo",
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _createdAt: string;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: string;
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _createdAt: Date;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: Date;
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "document",
 *   fields: [...],
 *   ...
 * };
 */

File

All file type properties pass through with the exceptions noted in Types and Types with Fields.

const type = s.file({
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _type: "file";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _type: "file";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "file",
 *   fields: [...],
 *   ...
 * };
 */

Geopoint

All geopoint type properties pass through with the exceptions noted in Types.

const type = s.geopoint();

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _type: "geopoint";
 *   alt: number;
 *   lat: number;
 *   lng: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _type: "geopoint";
 *   alt: number;
 *   lat: number;
 *   lng: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "geopoint",
 *   ...
 * };
 */

Image

All image type properties pass through with the exceptions noted in Types and Types with Fields.

Other exceptions include hotspot. Including hotspot: true adds the crop and hotspot properties in the infer types.

const type = s.image({
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _type: "image";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _type: "image";
 *   asset: {
 *     _type: "reference";
 *     _ref: string;
 *   };
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "image",
 *   fields: [...],
 *   ...
 * };
 */

Number

All number type properties pass through with the exceptions noted in Types.

Other exceptions include greaterThan, integer, lessThan, max, min, negative, positive, and precision. These values are used in the zod validations and the sanity validations.

const type = s.number();

type Value = s.infer<typeof type>;

/**
 * type Value === number;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === number;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "number",
 *   ...
 * };
 */

Object

All object type properties pass through with the exceptions noted in Types and Types with Fields.

const type = s.object({
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "object",
 *   fields: [...],
 *   ...
 * };
 */

Object (Named)

All object type properties pass through with the exceptions noted in Types and Types with Fields.

This is separate from s.object because, when objects are named in sanity, there are significant differences:

  • The value has a _type field equal to the object's name.
  • They can be used directly in schemas (like any other schema).
  • They can also be registered as a top level object and simply referenced by type within another schema.
const type = s.objectNamed({
  name: "aNamedObject",
  fields: [
    {
      name: "foo",
      type: s.number(),
    },
    {
      name: "bar",
      optional: true,
      type: s.number(),
    },
  ],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _type: "aNamedObject";
 *   foo: number;
 *   bar?: number;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _type: "aNamedObject";
 *   foo: number;
 *   bar?: number;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "object",
 *   fields: [...],
 *   ...
 * };
 */
// Use `.namedType()` to reference it in another schema.
const someOtherType = s.array({ of: [type.namedType()] });

// The reference value is used directly.
type SomeOtherValue = s.infer<typeof someOtherType>;

/**
 * type SomeOtherValue = [{
 *   _type: "aNamedObject";
 *   foo: number;
 *   bar?: number;
 * }];
 */

// The schema is made within the referencing schema
const someOtherTypeSchema = someOtherType.schema();

/**
 * const someOtherTypeSchema = {
 *   type: "array",
 *   of: [{ type: "" }],
 *   ...
 * };
 */

defineConfig({
  schema: {
    types: [type.schema(), someOtherType.schema()],
  },
});

Reference

All reference type properties pass through with the exceptions noted in Types.

Reference resolves into the referenced document's mock.

Other exceptions include weak. Including weak: true adds the _weak: true properties in the infer types.

const type = s.reference({
  to: [someDocumentType, someOtherDocumentType],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _ref: string;
 *   _type: "reference";
 *   _weak?: boolean;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _ref: string;
 *   _type: "reference";
 *   _weak?: boolean;
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "reference",
 *   to: [...],
 *   ...
 * };
 */
const type = s.reference({
  weak: true,
  to: [someDocumentType, someOtherDocumentType],
});

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _ref: string;
 *   _type: "reference";
 *   _weak: true;
 * };
 */

Slug

All slug type properties pass through with the exceptions noted in Types.

Slug parses into a string.

const type = s.slug();

type Value = s.infer<typeof type>;

/**
 * type Value === {
 *   _type: "slug";
 *   current: string;
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "slug",
 *   ...
 * };
 */

String

All string type properties pass through with the exceptions noted in Types.

Other exceptions include min, max, and length. These values are used in the zod validations and the sanity validations.

const type = s.string();

type Value = s.infer<typeof type>;

/**
 * type Value === string;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "string",
 *   ...
 * };
 */

Text

All text type properties pass through with the exceptions noted in Types.

Other exceptions include min, max, and length. These values are used in the zod validations and the sanity validations.

const type = s.text();

type Value = s.infer<typeof type>;

/**
 * type Value === string;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "text",
 *   ...
 * };
 */

URL

All url type properties pass through with the exceptions noted in Types.

const type = s.url();

type Value = s.infer<typeof type>;

/**
 * type Value === string;
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === string;
 */

const schema = type.schema();

/**
 * const schema = {
 *   type: "url",
 *   ...
 * };
 */

Additional Types

In addition to the default sanity schema types, you may have nonstandard types (custom asset sources like MUX Input or unique inputs like code input).

s.createType allows for creation of a custom type. It returns an object of type s.SanityType<Definition, Value, ParsedValue, ResolvedValue>. All provided s.* methods use this, so it should be fully featured for any use case.

An example using Mux Input (not including installing the plugin):

import { faker } from "@faker-js/faker";
import { z } from "zod";

import { s } from "@sanity-typed/schema-builder";

const muxVideo = () =>
  s.createType({
    // `schema` returns the sanity schema type
    schema: () => ({ type: "mux.video" } as const),

    // `mock` returns an instance of the native sanity value
    // `faker` will have a stable `seed` value
    mock: (faker) =>
      ({
        _type: "mux.video",
        asset: {
          _type: "reference",
          _ref: faker.datatype.uuid(),
        },
      } as const),

    // `zod` is used for parsing this type
    zod: z.object({
      _type: z.literal("mux.video"),
      asset: z.object({
        _type: z.literal("reference"),
        _ref: z.string(),
      }),
    }),

    // `zodResolved` is used for parsing into the resolved value
    // defaults to reusing `zod`
    zodResolved: z
      .object({
        _type: z.literal("mux.video"),
        asset: z.object({
          _type: z.literal("reference"),
          _ref: z.string(),
        }),
      })
      .transform(
        ({ asset: { _ref: playbackId } }) => resolvedValues[playbackId]
      ),
  });

const type = document({
  name: "foo",
  fields: [
    {
      name: "video",
      type: muxVideo(),
    },
  ],
});

const value = type.mock(faker);

/**
 * typeof value === {
 *   _createdAt: string;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: string;
 *   video: {
 *     _type: "mux.video";
 *     asset: {
 *       _ref: string;
 *       _type: "reference";
 *     };
 *   };
 * };
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * typeof parsedValue === {
 *   _createdAt: Date;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: Date;
 *   video: {
 *     _type: "mux.video";
 *     asset: {
 *       _ref: string;
 *       _type: "reference";
 *     };
 *   };
 * };
 */

const resolvedValue: s.resolved<typeof type> = type.resolve(value);

/**
 * typeof resolvedValue === {
 *   _createdAt: Date;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: Date;
 *   video: (typeof resolvedValues)[string];
 * };
 */

const schema = type.schema();

/**
 * const schema = {
 *   name: "foo",
 *   type: "document",
 *   fields: [
 *     {
 *       name: "video",
 *       type: "mux.video",
 *     },
 *   ],
 * };
 */

Parsing and zod

Due to sanity's transport layer being JSON (and whatever reason slug has for being wrapped in an object), some of sanity's return values require some transformation in application logic. Every type includes a .parse(value) method that transforms values to a more convenient value.

We accomplish that using Zod, a powerful schema validation library with full typescript support. A few of the types have default transformations (most notably s.datetime parsing into a javascript Date object). The zod types are available for customization, allowing your own transformations.

const type = s.document({
  name: "foo",
  // If you dislike the dangling underscore on `_id`, this transforms it to `id`:
  zod: (zod) => zod.transform(({ _id: id, ...doc }) => ({ id, ...doc })),
  fields: [
    {
      name: "aString",
      type: s.string(),
    },
    {
      name: "aStringLength",
      type: s.string({
        // For whatever reason, if you want the length of the string instead of the string itself:
        zod: (zod) => zod.transform((value) => value.length),
      }),
    },
    {
      name: "aDateTime",
      type: s.datetime(),
    },
    {
      name: "aSlug",
      type: s.slug(),
    },
  ],
});

const value: type Value === {
  /* ... */
};

/**
 * This remains the same:
 *
 * typeof value === {
 *   _createdAt: string;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: string;
 *   aString: string;
 *   aStringLength: string;
 *   aDateTime: string;
 *   aSlug: {
 *     _type: "slug";
 *     current: string;
 *   };
 * }
 */

const parsedValue: s.output<typeof type> = type.parse(value);

/**
 * Notice the changes:
 *
 * typeof parsedValue === {
 *   _createdAt: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: string;
 *   id: string;
 *   aString: string;
 *   aStringLength: number;
 *   aDateTime: Date;
 *   aSlug: string;
 * }
 */

Mocking

Sanity values are used directly in react components or application code that needs to be tested. While tests tend to need mocks that are specific to isolated tests, autogenerated mocks are extremely helpful. Every type includes a .mock(faker) method that generates mocks of that type.

We accomplish that using Faker, a powerful mocking library with full typescript support. All of the types have default mocks. The mock methods are available for customization, allowing your own mocks.

Note: Each type will create it's own instance of Faker with a seed based on it's path in the document, so mocked values for any field should remain consistent as long as it remains in the same position.

import { faker } from "@faker-js/faker";

const type = s.document({
  name: "foo",
  fields: [
    {
      name: "aString",
      type: s.string(),
    },
    {
      name: "aFirstName",
      type: s.string({
        mock: (faker) => faker.name.firstName(),
      }),
    },
  ],
});

const value = type.mock(faker);

/**
 * typeof value === {
 *   _createdAt: string;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: string;
 *   aString: string;
 *   aFirstName: string;
 * }
 *
 * value.aString === "Seamless"
 * value.aFirstName === "Katelynn"
 */

Resolving Mocks

Sanity values often reference something outside of itself, most notably s.reference referencing other documents. Applications determine how those resolutions happen (in the case of s.reference, usually via groq queries) but tests that require resolved values shouldn't rebuild that logic. Every type includes a .resolve(value) method that resolves mocks of that type.

We accomplish that using Zod, a powerful schema validation library with full typescript support. All of the types have default resolutions. The resolution methods are available for customization, allowing your own resolution.

import { faker } from "@faker-js/faker";

const barType = s.document({
  name: "bar",
  fields: [
    {
      name: "value",
      type: s.string(),
    },
  ],
});

const nonSanityMocks: Record<string, NonSanity> = {
  /* ... */
};

const type = s.document({
  name: "foo",
  fields: [
    {
      name: "bar",
      type: s.reference({ to: [barType] }),
    },
    {
      name: "aString",
      type: s.string(),
    },
    {
      name: "nonSanity",
      type: s.string({
        zodResolved: (zod) => zod.transform((value) => nonSanityMocks[value]!),
      }),
    },
  ],
});

const value = type.resolve(type.mock(faker));

/**
 * typeof value === {
 *   _createdAt: Date;
 *   _id: string;
 *   _rev: string;
 *   _type: "foo";
 *   _updatedAt: Date;
 *   bar: {
 *     _createdAt: Date;
 *     _id: string;
 *     _rev: string;
 *     _type: "bar";
 *     _updatedAt: Date;
 *     value: string;
 *   };
 *   aString: string;
 *   nonSanity: NonSanity;
 * }
 */

Readme

Keywords

none

Package Sidebar

Install

npm i @sanity-typed/schema-builder

Weekly Downloads

1,585

Version

3.0.1

License

none

Unpacked Size

118 kB

Total Files

5

Last publish

Collaborators

  • saiichihashimoto