import type { Infer } from './schema' import type { Expect, IsEqual } from './type-test' import { cloneJsonObject } from '../data' import { uuid } from '../uuid' import { schemaParseObject } from './parse-object' import { any, array, boolean, float, int, literal, number, object, record, string, stringLiterals, tuple, union } from './schema' import { z } from './z' describe('schema', () => { it('create schema', async () => { // Literals type Status = 'active' | 'trialing' | 'past_due' | 'paused' | 'deleted' const lit = stringLiterals(['active', 'trialing', 'past_due', 'paused', 'deleted']) type ScheamLiterals = Infer type SchemaLiteralsTest = Expect> // Should pass expectTypeOf().toMatchTypeOf() // Tuple const tup = tuple([number(), string(), boolean()]) type SchemaTuple = Infer // expected [number, string, boolean] type SchemaTupleTest = Expect> // Should pass expectTypeOf().toMatchTypeOf<[number, string, boolean]>() const s1 = string().optional() // .pattern(/\d+/) type t1a = typeof s1 type t1 = Infer expectTypeOf().toMatchTypeOf() type CustomType = string | number | boolean const schema = object({ id: string().default('123'), // default(() => '123'), name: string(), age: int().optional(), active: boolean(), tags: array(string()).optional(), info: any(), // status: stringLiterals(['active', 'trialing', 'past_due', 'paused', 'deleted']), // status: string(), obj: object({ test: float(), }).optional(), lit, log: any(), }) type Schema = Infer type SchemaTest = Expect> // Should pass const sample: Omit = { name: 'Hello', age: 42, active: true, lit: 'past_due', info: 123, log: true, } // const s: Status = sample.status expect(cloneJsonObject(schema)).toMatchInlineSnapshot(` Object { "_object": Object { "active": Object { "type": "boolean", }, "age": Object { "_optional": true, "type": "int", }, "id": Object { "_default": "123", "type": "string", }, "info": Object { "type": "any", }, "lit": Object { "_enumValues": Array [ "active", "trialing", "past_due", "paused", "deleted", ], "type": "string", }, "log": Object { "type": "any", }, "name": Object { "type": "string", }, "obj": Object { "_object": Object { "test": Object { "type": "number", }, }, "_optional": true, "type": "object", }, "tags": Object { "_optional": true, "_type": Object { "type": "string", }, "type": "array", }, }, "type": "object", } `) expect(schemaParseObject(schema, sample)).toMatchInlineSnapshot(` Object { "active": true, "age": 42, "id": "123", "info": 123, "lit": "past_due", "log": true, "name": "Hello", } `) // expect(schema.map(sample, (v, s) => { // if (s.type === 'boolean') { // return v ? 'on' : 'off' // } // })).toMatchInlineSnapshot(` // Object { // "active": "on", // "age": 42, // "info": 123, // "lit": "past_due", // "log": true, // "name": "Hello", // "obj": Object {}, // } // `) // expect(schema.map(sample, function (v) { // if (this.type === 'boolean') { // return v ? 'yes' : 'no' // } // })).toMatchInlineSnapshot(` // Object { // "active": "yes", // "age": 42, // "info": 123, // "lit": "past_due", // "log": true, // "name": "Hello", // "obj": Object {}, // } // `) // expect(schema.parse({} as any)).toBe() }) it('union', async () => { const literals = [ literal('one'), literal('two'), literal('three'), ] const schema = object({ id: string().default(() => '123').meta({ // someProp: 'someValue', }), literal: literal('demo'), name: union(literals), }) type Schema = Infer const sample: Partial = { literal: 'demo', name: 'two', } expect(cloneJsonObject(schema)).toMatchInlineSnapshot(` Object { "_object": Object { "id": Object { "_meta": Object {}, "type": "string", }, "literal": Object { "_default": "demo", "type": "literal", }, "name": Object { "_union": Array [ Object { "_default": "one", "type": "literal", }, Object { "_default": "two", "type": "literal", }, Object { "_default": "three", "type": "literal", }, ], "type": "union", }, }, "type": "object", } `) expect(schemaParseObject(schema, sample)).toMatchInlineSnapshot(` Object { "id": "123", "literal": "demo", "name": "two", } `) // expect(schema.parse({} as any)).toBe() }) it('union with object', async () => { const obj = union([ object({ subscription: literal(true), subscriptionId: string() }), object({ subscription: literal(false), licenseId: string() }), ]) type Schema = Infer type SchemaExpected = { subscription: true subscriptionId: string } | { subscription: false licenseId: string } type _SchemaTest = Expect> // Should pass expectTypeOf().toMatchTypeOf() }) it('mimic extend schema', async () => { const baseSchema = z.object({ id: string().default(uuid), }) const extendedSchema = baseSchema.extend({ name: string(), age: int().optional(), active: boolean(), tags: array(string()).optional(), info: any(), description: string().optional(), }) type BaseSchema = Infer type ExtendedSchema = Infer expectTypeOf().toMatchObjectType<{ id: string }>() expectTypeOf().toMatchObjectType<{ id: string age?: number | undefined tags?: string[] | undefined info?: any name: string active: boolean description?: string | undefined }>() }) it('optional and default', async () => { const s = z.object({ def1: z.string().default('hallo'), // not optional! def2: z.string().default('hallo').optional(), def3: z.string().optional().default('hallo'), // different order def4: z.string().optional(), }) type Schema = Infer type SchemaTest = Expect> // Should pass }) describe('object manipulation methods', () => { // Test setup - similar to the Recipe example const RecipeSchema = object({ title: string(), description: string().optional(), ingredients: array(string()), published: boolean(), }) expectTypeOf>().toMatchObjectType<{ description?: string | undefined title: string ingredients: string[] published: boolean }>() describe('pick()', () => { it('should pick specified keys from object schema', () => { const JustTheTitle = RecipeSchema.pick({ title: true }) // Type should only include title expect(JustTheTitle._object).toEqual({ title: expect.any(Object), }) expect(JustTheTitle._object.title).toBe(RecipeSchema._object.title) expectTypeOf>().toMatchObjectType<{ title: string }>() }) it('should pick multiple keys from object schema', () => { const TitleAndIngredients = RecipeSchema.pick({ title: true, ingredients: true }) expect(TitleAndIngredients._object).toEqual({ title: expect.any(Object), ingredients: expect.any(Object), }) expectTypeOf>().toMatchObjectType<{ title: string ingredients: string[] }>() }) it('should throw error when used on non-object schema', () => { const stringSchema = string() expect(() => stringSchema.pick({ anything: true } as any)).toThrow('pick() can only be used on object schemas') }) }) describe('omit()', () => { it('should omit specified keys from object schema', () => { const RecipeNoDescription = RecipeSchema.omit({ description: true }) // Should have all keys except description expect(Object.keys(RecipeNoDescription._object)).toEqual(['title', 'ingredients', 'published']) expect(RecipeNoDescription._object.description).toBeUndefined() expectTypeOf>().toMatchObjectType<{ title: string ingredients: string[] published: boolean }>() // const x: Infer = { // title: 'Test Recipe', // ingredients: ['Flour', 'Sugar'], // published: true, // description: 'This should not be here', // This should cause a type error // } }) it('should omit multiple keys from object schema', () => { const JustTitleAndPublished = RecipeSchema.omit({ description: true, ingredients: true }) expect(Object.keys(JustTitleAndPublished._object)).toEqual(['title', 'published']) expectTypeOf>().toMatchObjectType<{ title: string published: boolean }>() }) it('should throw error when used on non-object schema', () => { const stringSchema = string() expect(() => stringSchema.omit({ anything: true } as any)).toThrow('omit() can only be used on object schemas') }) }) describe('partial()', () => { it('should make all properties optional when no keys specified', () => { const PartialRecipe = RecipeSchema.partial() // All properties should be optional expect(PartialRecipe._object.title._optional).toBe(true) expect(PartialRecipe._object.description._optional).toBe(true) // was already optional expect(PartialRecipe._object.ingredients._optional).toBe(true) expect(PartialRecipe._object.published._optional).toBe(true) expectTypeOf>().toMatchObjectType<{ title?: string ingredients?: string[] published?: boolean description?: string }>() }) it('should make only specified properties optional', () => { const RecipeOptionalIngredients = RecipeSchema.partial({ ingredients: true }) // Only ingredients should be made optional, others retain original state expect(RecipeOptionalIngredients._object.title._optional).toBeUndefined() // was required, stays required expect(RecipeOptionalIngredients._object.description._optional).toBe(true) // was optional, stays optional expect(RecipeOptionalIngredients._object.ingredients._optional).toBe(true) // was required, now optional expect(RecipeOptionalIngredients._object.published._optional).toBeUndefined() // was required, stays required expectTypeOf>().toMatchObjectType<{ title: string description?: string | undefined ingredients?: string[] | undefined published: boolean }>() }) it('should throw error when used on non-object schema', () => { const stringSchema = string() expect(() => stringSchema.partial()).toThrow('partial() can only be used on object schemas') }) }) describe('required()', () => { it('should make all properties required when no keys specified', () => { const RequiredRecipe = RecipeSchema.required() // All properties should be required (not optional) expect(RequiredRecipe._object.title._optional).toBe(false) expect(RequiredRecipe._object.description._optional).toBe(false) expect(RequiredRecipe._object.ingredients._optional).toBe(false) expect(RequiredRecipe._object.published._optional).toBe(false) expectTypeOf>().toMatchObjectType<{ title: string ingredients: string[] published: boolean description: string }>() }) it('should make only specified properties required', () => { const RecipeRequiredDescription = RecipeSchema.required({ description: true }) // Only description should be made required, others retain original state expect(RecipeRequiredDescription._object.title._optional).toBeUndefined() // was required, stays required expect(RecipeRequiredDescription._object.description._optional).toBe(false) // was optional, now required expect(RecipeRequiredDescription._object.ingredients._optional).toBeUndefined() // was required, stays required expect(RecipeRequiredDescription._object.published._optional).toBeUndefined() // was required, stays required expectTypeOf>().toMatchObjectType<{ title: string description: string ingredients: string[] published: boolean }>() }) it('should throw error when used on non-object schema', () => { const stringSchema = string() expect(() => stringSchema.required()).toThrow('required() can only be used on object schemas') }) }) }) it('complex union with CSP example', async () => { const csp = union([ boolean(), stringLiterals(['strict', 'moderate', 'permissive', 'disabled']), string(), record(union([string(), array(string()), boolean()])), ]).default(false).meta({ desc: 'Content Security Policy: boolean, preset (strict/moderate/permissive/disabled), string directive, or object' }) type CSP = Infer type CSPExpected = | boolean | 'strict' | 'moderate' | 'permissive' | 'disabled' | string | Record type CSPTest = Expect> expectTypeOf().toMatchTypeOf() const x: CSP = 'xxx' // Test boolean value expect(schemaParseObject(object({ csp }), { csp: true })).toEqual({ csp: true }) expect(schemaParseObject(object({ csp }), { csp: false })).toEqual({ csp: false }) // Test enum values expect(schemaParseObject(object({ csp }), { csp: 'strict' })).toEqual({ csp: 'strict' }) expect(schemaParseObject(object({ csp }), { csp: 'moderate' })).toEqual({ csp: 'moderate' }) expect(schemaParseObject(object({ csp }), { csp: 'permissive' })).toEqual({ csp: 'permissive' }) expect(schemaParseObject(object({ csp }), { csp: 'disabled' })).toEqual({ csp: 'disabled' }) // Test string directive expect(schemaParseObject(object({ csp }), { csp: 'default-src \'self\'' })).toEqual({ csp: 'default-src \'self\'' }) // Test object with various value types const cspObject = { 'default-src': '\'self\'', 'script-src': ['https://cdn.example.com', '\'unsafe-inline\''], 'upgrade-insecure-requests': true, } expect(schemaParseObject(object({ csp }), { csp: cspObject })).toEqual({ csp: cspObject }) // Test default value expect(schemaParseObject(object({ csp }), {})).toEqual({ csp: false }) // Test metadata expect(csp._meta).toEqual({ desc: 'Content Security Policy: boolean, preset (strict/moderate/permissive/disabled), string directive, or object' }) }) })