UNPKG

cogsbox-shape

Version:

A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for r

1,154 lines (1,153 loc) 67.6 kB
import { expect, describe, it } from "vitest"; import { expectTypeOf } from "expect-type"; // Import the new primary method for schema creation import { s, schema, createSchemaBox, addViews } from "../schema.js"; import z from "zod"; /* ================================================================ SECTION A: TYPE-LEVEL TESTS (Using the new pattern) ================================================================ */ describe("Schema Builder Type Tests (with expect-type)", () => { describe("Basic Field Definitions", () => { it("should correctly type a simple varchar field", () => { const nameField = s.sqlite({ type: "varchar" }); expectTypeOf(nameField.config.zodSqlSchema).toEqualTypeOf(); }); it("should correctly type a nullable integer field", () => { const ageField = s.sqlite({ type: "int", nullable: true }); expectTypeOf(ageField.config.zodClientCheckedSchema).toEqualTypeOf(); }); it("should correctly type a primary key field", () => { const idField = s.sqlite({ type: "int", pk: true }); expectTypeOf(idField.config.zodSqlSchema).toEqualTypeOf(); }); it("should correctly type an enum field", () => { const statusField = s.sqlite({ type: "enum", values: ["draft", "published", "archived"], }); expectTypeOf().toEqualTypeOf(); expect(statusField.config.zodSqlSchema.parse("draft")).toBe("draft"); expect(() => statusField.config.zodSqlSchema.parse("deleted")).toThrow(); }); it("should correctly type a real (float) field", () => { const tempField = s.sqlite({ type: "real" }); expectTypeOf(tempField.config.zodSqlSchema).toEqualTypeOf(); expect(tempField.config.zodSqlSchema.parse(3.14)).toBe(3.14); }); it("should correctly type a nullable real field", () => { const nullableReal = s.sqlite({ type: "real", nullable: true }); expectTypeOf(nullableReal.config.zodClientCheckedSchema).toEqualTypeOf(); expect(nullableReal.config.zodSqlSchema.parse(null)).toBeNull(); expect(nullableReal.config.zodSqlSchema.parse(2.5)).toBe(2.5); }); }); describe("Chainable Methods", () => { it("should create a union type when .client provides a different type", () => { const idField = s.sqlite({ type: "int", pk: true }).client({ value: () => "temp-uuid-123", schema: z.literal("temp-uuid-123"), }); expectTypeOf().toEqualTypeOf(); }); it("should NOT create a union type when .client provides the same type", () => { const countField = s .sqlite({ type: "int" }) .client({ value: () => 0, schema: z.number() }); expectTypeOf().toEqualTypeOf(); }); it("should correctly override the client schema with .client()", () => { const statusField = s .sqlite({ type: "int" }) .client(() => z.boolean()); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); it("should add validation to client schema with .client()", () => { const nameField = s .sqlite({ type: "varchar" }) .client({ value: "John" }) .clientCheck((tools) => tools.client.min(3)); expectTypeOf().toEqualTypeOf(); }); it("should chain .client().clientCheck().server() correctly", () => { const nameField = s .sqlite({ type: "varchar" }) .client({ value: "" }) .clientCheck((tools) => tools.client.min(3)) .server((tools) => tools.client.min(5)); expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); }); describe("`createSchemaBoxRegistry` Integration with Relations", () => { // 1. Define schemas with placeholders const users = schema({ _tableName: "users", id: s.sqlite({ type: "int", pk: true }).client({ value: () => "new-user", schema: z.literal("new-user"), }), posts: s.hasMany(), }); const posts = schema({ _tableName: "posts", id: s.sqlite({ type: "int", pk: true }), isPublished: s.sqlite({ type: "int" }).client(() => z.boolean()), authorId: s.reference(() => users.id), }); // 2. Create the registry and resolve relations const box = createSchemaBox({ users, posts }, { users: { posts: { fromKey: "id", toKey: posts.authorId }, }, }); const finalPostResult = box.posts.schemas; it("should correctly handle reference types", () => { // The authorId should be a union of the DB type (number) and the initial state of the referenced field expectTypeOf().toEqualTypeOf(); }); }); }); /* ================================================================ SECTION B: RUNTIME BEHAVIOR TESTS (Using the new pattern) ================================================================ */ describe("Schema Builder Runtime Behavior", () => { describe("Default Value Generation", () => { // Define the schema using the new builder syntax const defaultsSchema = schema({ _tableName: "defaults", fromInitialState: s.sqlite({ type: "varchar" }).client({ value: () => "from-initial-state", schema: z.string(), }), fromSqlDefault: s.sqlite({ type: "int", default: 99 }), isNullable: s.sqlite({ type: "boolean", nullable: true }), hasNoDefault: s.sqlite({ type: "int" }), }); // Process it with the registry const box = createSchemaBox({ defaults: defaultsSchema }, { defaults: {}, }); const defaults = box.defaults.defaults; it("should get default from .client()", () => { expect(defaults.fromInitialState).toBe("from-initial-state"); }); it("should get default from SQL config", () => { expect(defaults.fromSqlDefault).toBe(99); }); it("should default a nullable field to null", () => { expect(defaults.isNullable).toBeNull(); }); it("should use the generated default (e.g., 0 for int) when none is provided", () => { expect(defaults.hasNoDefault).toBe(0); }); }); describe("Schema Parsing, Validation, and Transformation", () => { const complexSchemaDef = schema({ _tableName: "complex", id: s.sqlite({ type: "int", pk: true }), status: s .sqlite({ type: "int" }) // DB: 0=Inactive, 1=Active .client(() => z.enum(["inactive", "active"])) // Client: "inactive" | "active" .transform({ toClient: (dbValue) => (dbValue === 1 ? "active" : "inactive"), toDb: (clientValue) => (clientValue === "active" ? 1 : 0), }), name: s .sqlite({ type: "varchar" }) .server(({ sql }) => sql.min(3, "Name is too short")), }); // Use the new registry to process the schema const box = createSchemaBox({ complex: complexSchemaDef }, { complex: {}, }); const { client: clientChecked, sql, server } = box.complex.schemas; const { toClient, toDb } = box.complex.transforms; it("should correctly transform a DB object to a Client object", () => { const dbData = { id: 1, status: 1, name: "Test" }; const clientResult = toClient(dbData); expect(clientResult.status).toBe("active"); expect(() => clientChecked.parse(clientResult)).not.toThrow(); }); it("should correctly transform a Client object to a DB object", () => { const clientData = { id: 1, status: "inactive", name: "Test" }; const dbResult = toDb(clientData); expect(dbResult.status).toBe(0); expect(() => sql.parse(dbResult)).not.toThrow(); }); it("should still use the validationSchema for pure validation", () => { const invalidClientData = { id: 1, status: "inactive", name: "ab" }; const result = server.safeParse(invalidClientData); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues[0].message).toBe("Name is too short"); } }); }); }); describe("Tools params are actual Zod schemas at runtime", () => { it("should provide actual Zod schemas as tools in .client()", () => { let capturedTools; s.sqlite({ type: "varchar" }) .client({ value: "test" }) .clientCheck((tools) => { capturedTools = { sql: tools.sql, client: tools.client, clientCheck: tools.clientCheck, }; return tools.clientCheck; }); expect(capturedTools).toBeDefined(); expect(capturedTools.sql).toBeInstanceOf(z.ZodType); expect(typeof capturedTools.sql.parse).toBe("function"); expect(typeof capturedTools.sql.safeParse).toBe("function"); expect(capturedTools.client).toBeInstanceOf(z.ZodType); expect(typeof capturedTools.client.parse).toBe("function"); expect(capturedTools.clientCheck).toBeInstanceOf(z.ZodType); expect(typeof capturedTools.clientCheck.parse).toBe("function"); }); it("should provide actual Zod schemas as tools in .server()", () => { let capturedTools; s.sqlite({ type: "varchar" }) .client({ value: "test" }) .clientCheck((tools) => tools.client) .server((tools) => { capturedTools = { sql: tools.sql, client: tools.client, clientCheck: tools.clientCheck, }; return tools.clientCheck; }); expect(capturedTools).toBeDefined(); expect(capturedTools.sql).toBeInstanceOf(z.ZodType); expect(capturedTools.client).toBeInstanceOf(z.ZodType); expect(capturedTools.clientCheck).toBeInstanceOf(z.ZodType); }); it("should allow calling Zod methods on tools params in .client()", () => { let clientSchema; s.sqlite({ type: "varchar" }) .client({ value: "" }) .clientCheck((tools) => { clientSchema = tools.client; const validated = tools.client.min(3); expect(validated.safeParse("ab").success).toBe(false); expect(validated.safeParse("abc").success).toBe(true); return validated; }); expect(clientSchema).toBeInstanceOf(z.ZodString); }); it("should allow calling Zod methods on tools params in .server()", () => { let serverClientSchema; s.sqlite({ type: "varchar" }) .client({ value: "" }) .clientCheck((tools) => tools.client.min(3)) .server((tools) => { serverClientSchema = tools.client; const validated = tools.client.min(5); expect(validated.safeParse("abcd").success).toBe(false); expect(validated.safeParse("abcde").success).toBe(true); return validated; }); expect(serverClientSchema).toBeInstanceOf(z.ZodString); }); it("should provide correct Zod types for numeric fields in tools", () => { let capturedSql; let capturedInput; s.sqlite({ type: "int" }) .client({ value: 0 }) .clientCheck((tools) => { capturedSql = tools.sql; capturedInput = tools.client; return tools.client; }); expect(capturedSql).toBeInstanceOf(z.ZodNumber); expect(capturedSql.parse(42)).toBe(42); expect(capturedInput).toBeInstanceOf(z.ZodNumber); expect(capturedInput.parse(0)).toBe(0); }); it("should provide correct Zod types for boolean fields in tools", () => { let capturedInput; s.sqlite({ type: "int" }) .client(() => z.boolean()) .clientCheck((tools) => { capturedInput = tools.client; return tools.client; }); expect(capturedInput).toBeInstanceOf(z.ZodBoolean); expect(capturedInput.parse(true)).toBe(true); expect(capturedInput.safeParse("not bool").success).toBe(false); }); it("should allow nullable() and other modifiers on tools params", () => { let capturedInput; s.sqlite({ type: "varchar", nullable: true }) .client({ value: null, schema: z.string().nullable() }) .clientCheck((tools) => { capturedInput = tools.client; expect(tools.client.safeParse(null).success).toBe(true); expect(tools.client.safeParse("hello").success).toBe(true); return tools.client; }); expect(capturedInput).toBeInstanceOf(z.ZodNullable); }); }); describe("client vs clientChecked schema divergence after .clientCheck()", () => { it("should retain unmodified client schema after .clientCheck() adds .min()", () => { const nameField = s.sqlite({ type: "varchar" }) .client({ value: "John" }) .clientCheck((tools) => tools.client.min(3)); const clientSchema = nameField.config.zodClientSchema; const clientCheckedSchema = nameField.config.zodClientCheckedSchema; expect(clientSchema.safeParse("ab").success).toBe(true); expect(clientCheckedSchema.safeParse("ab").success).toBe(false); expect(clientCheckedSchema.safeParse("abc").success).toBe(true); }); it("should diverge at box schema level after .clientCheck()", () => { const users = schema({ _tableName: "users", id: s.sqlite({ type: "int", pk: true }), name: s.sqlite({ type: "varchar" }) .client({ value: "" }) .clientCheck((tools) => tools.client.min(3)), }); const box = createSchemaBox({ users }, { users: {} }); expect(box.users.schemas.client.shape.name.safeParse("ab").success).toBe(true); expect(box.users.schemas.clientChecked.shape.name.safeParse("ab").success).toBe(false); expect(box.users.schemas.clientChecked.shape.name.safeParse("abc").success).toBe(true); }); it("should apply .email() only to clientChecked schema, not client", () => { const nameField = s.sqlite({ type: "varchar" }) .client({ value: "" }) .clientCheck((tools) => tools.client.email()); expect(nameField.config.zodClientSchema.safeParse("not-an-email").success).toBe(true); expect(nameField.config.zodClientCheckedSchema.safeParse("not-an-email").success).toBe(false); expect(nameField.config.zodClientCheckedSchema.safeParse("a@b.com").success).toBe(true); }); it("should apply .max() only to clientChecked schema, not client", () => { const countField = s.sqlite({ type: "int" }) .client({ value: () => 0, schema: z.number() }) .clientCheck((tools) => tools.client.max(100)); expect(countField.config.zodClientSchema.safeParse(200).success).toBe(true); expect(countField.config.zodClientCheckedSchema.safeParse(200).success).toBe(false); expect(countField.config.zodClientCheckedSchema.safeParse(50).success).toBe(true); }); it("should produce different schemas across all three layers: client, clientChecked, server", () => { const nameField = s.sqlite({ type: "varchar" }) .client({ value: "" }) .clientCheck((tools) => tools.client.min(3)) .server((tools) => tools.client.min(5)); expect(nameField.config.zodClientSchema.safeParse("a").success).toBe(true); expect(nameField.config.zodClientCheckedSchema.safeParse("a").success).toBe(false); expect(nameField.config.zodClientCheckedSchema.safeParse("abc").success).toBe(true); expect(nameField.config.zodValidationSchema.safeParse("abc").success).toBe(false); expect(nameField.config.zodValidationSchema.safeParse("abcde").success).toBe(true); }); it("should preserve clientInputZod on the internal builder config after .clientCheck()", () => { let capturedConfig; const field = s.sqlite({ type: "varchar" }) .client({ value: "x" }) .clientCheck((tools) => { capturedConfig = { clientInputZod: tools.clientInputZod }; return tools.client.min(3); }); expect(field.config.zodClientSchema).toBeDefined(); expect(field.config.zodClientCheckedSchema).toBeDefined(); expect(field.config.zodClientSchema).not.toBe(field.config.zodClientCheckedSchema); }); it("should diverge when refine targets only 'clientCheck' layer, not 'client'", () => { const forms = schema({ _tableName: "forms", id: s.sqlite({ type: "int", pk: true }), password: s.sqlite({ type: "varchar" }).client({ value: "" }), confirmPassword: s.sqlite({ type: "varchar" }).client({ value: "" }), }).refine((r) => [ r("clientCheck", (row) => { if (row.password !== row.confirmPassword) { return { path: ["confirmPassword"], message: "Passwords must match" }; } return undefined; }), ]); const box = createSchemaBox({ forms }, { forms: {} }); const clientResult = box.forms.schemas.client.safeParse({ id: 1, password: "abc", confirmPassword: "def", }); expect(clientResult.success).toBe(true); const clientCheckedResult = box.forms.validators.clientChecked.safeParse({ id: 1, password: "abc", confirmPassword: "def", }); expect(clientCheckedResult.success).toBe(false); }); }); describe("New Session Features - Base Schema Without Relations", () => { const users = schema({ _tableName: "users", id: s .sqlite({ type: "int", pk: true }) .client({ value: () => "user-123", schema: z.string() }), petId: s.reference(() => pets.id), pets: s.hasMany(), }); const pets = schema({ _tableName: "pets", id: s.sqlite({ type: "int", pk: true }), userId: s.reference(() => users.id), owner: s.hasOne(), }); const box = createSchemaBox({ users, pets }, { users: { pets: { fromKey: "id", toKey: pets.userId }, }, pets: { owner: { fromKey: "userId", toKey: users.id }, }, }); describe("Base Schema Excludes Relations", () => { it("should exclude relations from base client schema", () => { expectTypeOf().toEqualTypeOf(); // Runtime check - the schema shape should not include 'pets' const clientShape = box.users.schemas.clientChecked.shape; expect(clientShape).not.toHaveProperty("pets"); expect(clientShape).toHaveProperty("id"); expect(clientShape).toHaveProperty("petId"); }); it("should exclude relations from default values", () => { const defaults = box.pets.defaults; // Runtime check expect(defaults).not.toHaveProperty("owner"); expect(defaults).toHaveProperty("id"); expect(defaults).toHaveProperty("userId"); }); }); describe("View Creation", () => { it("should include only selected relations in view", () => { const userView = box.users.createView({ pets: true, }); expectTypeOf().toEqualTypeOf(); // Runtime check const viewShape = userView.schemas.clientChecked.shape; expect(viewShape).toHaveProperty("pets"); expect(viewShape.pets).toBeInstanceOf(z.ZodArray); }); it("should handle nested relations correctly", () => { const userViewNested = box.users.createView({ pets: { owner: true }, }); expectTypeOf().toEqualTypeOf(); // Runtime check - owner should not have pets const shape = userViewNested.schemas.clientChecked.shape; if (shape.pets instanceof z.ZodArray) { const petSchema = shape.pets.element; if (petSchema instanceof z.ZodObject) { const petShape = petSchema.shape; expect(petShape).toHaveProperty("owner"); // Check that owner is optional expect(petShape.owner).toBeInstanceOf(z.ZodNullable); // Check that owner doesn't include pets if (petShape.owner instanceof z.ZodNullable) { const ownerSchema = petShape.owner._def.innerType; if (ownerSchema instanceof z.ZodObject) { expect(ownerSchema.shape).not.toHaveProperty("pets"); } } } } }); }); it("should provide type-safe navigation", () => { expectTypeOf().not.toBeNever(); // Runtime test - nav proxy should return nested proxies expect(box.users.nav).toBeDefined(); expect(box.users.nav.pets).toBeDefined(); expect(box.users.nav.pets.owner).toBeDefined(); }); describe("Default Values Accessibility", () => { it("should expose default values at top level", () => { expect(box.users.defaults).toBeDefined(); expect(box.users.defaults.id).toBe("user-123"); expect(box.pets.defaults.id).toBe(0); expect(box.pets.defaults.userId).toBe("user-123"); }); }); describe("Reference Resolution", () => { it("should correctly resolve reference types", () => { expectTypeOf().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); }); }); }); describe("Relation Defaults in Views", () => { const users = schema({ _tableName: "users", id: s .sqlite({ type: "int", pk: true }) .client({ value: () => "user-123", schema: z.string() }), name: s.sqlite({ type: "varchar" }).client({ value: "John" }), posts: s.hasMany({ count: 2 }), // Should generate 2 posts comments: s.hasMany([]), // Should be empty array profile: s.hasOne(true), // Changed from {} to true settings: s.hasOne(null), // Should be null followers: s.hasMany(undefined), // Should not be included }); const posts = schema({ _tableName: "posts", id: s.sqlite({ type: "int", pk: true }), title: s.sqlite({ type: "varchar" }).client({ value: "Default Post" }), authorId: s.reference(() => users.id), user: s.hasOne(true), }); const comments = schema({ _tableName: "comments", id: s.sqlite({ type: "int", pk: true }), text: s .sqlite({ type: "varchar" }) .client({ value: "Default Comment" }), userId: s.reference(() => users.id), user: s.hasOne(true), }); const profiles = schema({ _tableName: "profiles", id: s.sqlite({ type: "int", pk: true }), bio: s.sqlite({ type: "varchar" }).client({ value: "Default Bio" }), userId: s.reference(() => users.id), user: s.hasOne(true), }); const box = createSchemaBox({ users, posts, comments, profiles }, { users: { posts: { fromKey: "id", toKey: posts.authorId }, comments: { fromKey: "id", toKey: comments.userId }, profile: { fromKey: "id", toKey: profiles.userId }, settings: { fromKey: "id", toKey: profiles.userId }, followers: { fromKey: "id", toKey: users.id }, }, posts: { user: { fromKey: "id", toKey: users.id }, }, comments: { user: { fromKey: "id", toKey: users.id }, }, profiles: { user: { fromKey: "id", toKey: users.id }, }, }); it("should generate correct defaults based on relation config", () => { const view = box.users.createView({ posts: true, comments: true, profile: true, settings: true, followers: true, }); const defaults = view.defaults(); // Base fields expect(defaults.id).toBe("user-123"); expect(defaults.name).toBe("John"); // posts with count: 2 expect(defaults.posts).toHaveLength(2); expect(defaults.posts?.[0]).toEqual({ id: 0, title: "Default Post", authorId: "user-123", }); expect(defaults.posts?.[1]).toEqual({ id: 0, title: "Default Post", authorId: "user-123", }); // comments with [] expect(defaults.comments).toEqual([]); // profile with {} expect(defaults.profile).toEqual({ id: 0, bio: "Default Bio", userId: "user-123", }); // settings with null expect(defaults.settings).toBeNull(); // followers with undefined - should not exist expect(defaults).not.toHaveProperty("followers"); }); it("should handle nested relation defaults", () => { const nestedView = box.users.createView({ posts: {}, }); const defaults = nestedView.defaults(); expect(defaults.posts).toHaveLength(2); expect(defaults.posts?.[0]).not.toHaveProperty("author"); // Relation not selected }); }); describe("Client defaults and transforms", () => { const userSchema = schema({ _tableName: "users", id: s.sqlite({ type: "int", pk: true }).client({ value: () => `temp_${Math.random().toString(36).substr(2, 9)}`, schema: z.string(), }), email: s .sqlite({ type: "varchar", length: 255 }) .server(() => z.email("Invalid email address")), isActive: s .sqlite({ type: "int" }) .client(() => z.boolean()) .transform({ toClient: (val) => val === 1, toDb: (val) => (val ? 1 : 0), }), role: s.sqlite({ type: "varchar" }).client({ value: "user", schema: z.enum(["user", "admin"]), }), }); const box = createSchemaBox({ users: userSchema }, { users: {} }); const { transforms } = box.users; const defaults = box.users.generateDefaults(); it("should infer schema-only client defaults without running toClient", () => { expect(defaults.isActive).toBe(false); expect(typeof defaults.isActive).toBe("boolean"); }); it("should correctly transform defaults to db format", () => { const dbVersion = transforms.toDb(defaults); expect(dbVersion.isActive).toBe(0); expect(typeof dbVersion.isActive).toBe("number"); const clientVersion = transforms.toClient(dbVersion); expect(clientVersion.isActive).toBe(false); expect(typeof clientVersion.isActive).toBe("boolean"); }); it("should prefer explicit client defaults over inferred defaults", () => { const explicitDefaults = createSchemaBox({ users: schema({ _tableName: "users", score: s.sqlite({ type: "int" }).client({ value: 7, schema: z.number().min(0), }), isActive: s.sqlite({ type: "int" }).client({ value: true, schema: z.boolean(), }), }), }, { users: {} }).users.generateDefaults(); expect(explicitDefaults.score).toBe(7); expect(explicitDefaults.isActive).toBe(true); }); it("should not use toClient while generating defaults", () => { const defaults = createSchemaBox({ users: schema({ _tableName: "users", isActive: s .sqlite({ type: "int" }) .client(() => z.boolean()) .transform({ toClient: () => { throw new Error("toClient should not run for defaults"); }, toDb: (value) => (value ? 1 : 0), }), }), }, { users: {} }).users.generateDefaults(); expect(defaults.isActive).toBe(false); }); }); describe("UUID generation in initialState", () => { it("should call value function with uuid tool", () => { const field = s.sqlite({ type: "int", pk: true }).client({ value: ({ uuid }) => uuid(), schema: z.string(), clientPk: true, }); expect(typeof field.config.initialValue).toBe("function"); const result = field.config.initialValue({ uuid: () => "test-uuid", }); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); }); describe("Missing properties - parseForDb, parseFromDb, pk, clientPk, isClientRecord", () => { const users = schema({ _tableName: "users", id: s.sqlite({ type: "int", pk: true }).client({ value: ({ uuid }) => uuid(), schema: z.string(), clientPk: true, }), name: s.sqlite({ type: "varchar" }).client({ value: "John" }), isActive: s .sqlite({ type: "int" }) .client(() => z.boolean()) .transform({ toClient: (val) => val === 1, toDb: (val) => (val ? 1 : 0), }), email: s.sqlite({ type: "varchar", field: "email_address" }), posts: s.hasMany({ count: 1 }), }); const posts = schema({ _tableName: "posts", id: s.sqlite({ type: "int", pk: true }), title: s.sqlite({ type: "varchar" }).client({ value: "Untitled" }), authorId: s.reference(() => users.id), }); const box = createSchemaBox({ users, posts }, { users: { posts: { fromKey: "id", toKey: posts.authorId }, }, }); describe("DB Field Key Mapping (field property)", () => { it("should correctly map 'field' property to sqlSchema types and runtime shapes", () => { expectTypeOf().toHaveProperty("email_address"); expectTypeOf().not.toHaveProperty("email"); expectTypeOf().toHaveProperty("email"); expectTypeOf().not.toHaveProperty("email_address"); const sqlKeys = Object.keys(box.users.schemas.sql.shape); expect(sqlKeys).toContain("email_address"); expect(sqlKeys).not.toContain("email"); const clientKeys = Object.keys(box.users.schemas.clientChecked.shape); expect(clientKeys).toContain("email"); expect(clientKeys).not.toContain("email_address"); }); it("parseForDb should return an object with the DB keys typed correctly", () => { const clientData = { id: 1, name: "Alice", isActive: true, email: "alice@test.com", }; const dbData = box.users.transforms.parseForDb(clientData); expectTypeOf(dbData).toHaveProperty("email_address"); expectTypeOf(dbData).not.toHaveProperty("email"); expect(dbData.email_address).toBe("alice@test.com"); expect(dbData.email).toBeUndefined(); }); }); describe("pk and clientPk", () => { it("should expose pk array on base schema", () => { expect(box.users.pk).toEqual(["id"]); expect(box.posts.pk).toEqual(["id"]); }); it("should expose clientPk array for fields with clientPk: true", () => { expect(box.users.clientPk).toEqual(["id"]); }); it("should inherit clientPk on reference fields", () => { expect(box.posts.clientPk).toEqual(["authorId"]); }); }); describe("isClientRecord", () => { it("should auto-detect client records when clientPk is string but sql is int", () => { expect(box.users.isClientRecord).toBeDefined(); expect(typeof box.users.isClientRecord).toBe("function"); expect(box.users.isClientRecord({ id: "some-uuid", name: "Test" })).toBe(true); expect(box.users.isClientRecord({ id: 42, name: "Test" })).toBe(false); }); it("should also have isClientRecord on posts due to inherited clientPk from reference", () => { expect(box.posts.isClientRecord).toBeDefined(); expect(typeof box.posts.isClientRecord).toBe("function"); }); }); describe("parseFromDb and parseForDb", () => { it("should map DB column names to client keys on parseFromDb", () => { const dbRow = { id: 1, name: "Alice", isActive: 1, email_address: "a@b.com", }; const result = box.users.transforms.parseFromDb(dbRow); expect(result.email).toBe("a@b.com"); expect(result.isActive).toBe(true); }); it("should map client keys to DB column names on parseForDb", () => { const clientData = { id: 1, name: "Alice", isActive: true, email: "a@b.com", }; const result = box.users.transforms.parseForDb(clientData); expect(result.isActive).toBe(1); expect(result.email_address).toBe("a@b.com"); }); }); }); // ======================================================= // NEW TEST SUITE: Smart ClientPK & Mapped DB Keys Logic // ======================================================= describe("Smart clientPk and isClientRecord logic", () => { const smartSchema = schema({ _tableName: "smart_table", // 1. Auto-detect function execution id: s.sqlite({ type: "int", pk: true }).client({ value: ({ uuid }) => uuid(), schema: z.string(), clientPk: true, // Should auto-detect by dummy-executing the uuid factory }), // 2. Custom function + mapped db key mappedId: s.sqlite({ type: "int", field: "db_mapped_id" }).client({ value: "temp_999", schema: z.string(), clientPk: (val) => typeof val === "string" && val.startsWith("temp_"), }), }); const smartBox = createSchemaBox({ smart: smartSchema }, { smart: {}, }); it("should auto-detect client records by dummy-executing the factory function", () => { // Because value: ({ uuid }) => uuid() returns a string, the system should auto-infer `typeof val === "string"` expect(smartBox.smart.isClientRecord({ id: "some-uuid-123" })).toBe(true); expect(smartBox.smart.isClientRecord({ id: 1 })).toBe(false); }); it("should use a custom checker function if provided to clientPk", () => { // Valid custom format expect(smartBox.smart.isClientRecord({ mappedId: "temp_abc" })).toBe(true); // Invalid custom format (doesn't start with temp_) expect(smartBox.smart.isClientRecord({ mappedId: "invalid_abc" })).toBe(false); // Integer is not a client record expect(smartBox.smart.isClientRecord({ mappedId: 42 })).toBe(false); }); it("should safely check BOTH the client key and the db key at runtime", () => { // Provide ONLY the client key expect(smartBox.smart.isClientRecord({ mappedId: "temp_client" })).toBe(true); // Provide ONLY the db key (how it might look coming straight from a SQL query) expect(smartBox.smart.isClientRecord({ db_mapped_id: "temp_db" })).toBe(true); // Ensure failures correctly resolve when testing either key expect(smartBox.smart.isClientRecord({ mappedId: 100 })).toBe(false); expect(smartBox.smart.isClientRecord({ db_mapped_id: 100 })).toBe(false); }); }); describe("Nested relations with transforms", () => { const users = schema({ _tableName: "users", id: s .sqlite({ type: "int", pk: true }) .client({ value: () => "user-123", schema: z.string() }), name: s.sqlite({ type: "varchar" }).client({ value: "John" }), posts: s.hasMany({ count: 1 }), }); const posts = schema({ _tableName: "posts", id: s.sqlite({ type: "int", pk: true }), title: s.sqlite({ type: "varchar" }).client({ value: "Default Post" }), isPublished: s .sqlite({ type: "int" }) .client(() => z.boolean()) .transform({ toClient: (val) => val === 1, toDb: (val) => (val ? 1 : 0), }), authorId: s.reference(() => users.id), comments: s.hasMany({ count: 1 }), }); const comments = schema({ _tableName: "comments", id: s.sqlite({ type: "int", pk: true }), text: s.sqlite({ type: "varchar" }).client({ value: "Comment" }), isDeleted: s .sqlite({ type: "int" }) .client(() => z.boolean()) .transform({ toClient: (val) => val === 1, toDb: (val) => (val ? 1 : 0), }), postId: s.reference(() => posts.id), }); const box = createSchemaBox({ users, posts, comments }, { users: { posts: { fromKey: "id", toKey: posts.authorId }, }, posts: { comments: { fromKey: "id", toKey: comments.postId }, }, }); it("should transform nested relation fields in defaults", () => { const view = box.users.createView({ posts: { comments: true }, }); const defaults = view.defaults(); expect(defaults.posts).toHaveLength(1); const post = defaults.posts?.[0]; expect(post).toBeDefined(); expect(post.isPublished).toBe(false); expect(typeof post.isPublished).toBe("boolean"); expect(post.comments).toHaveLength(1); const comment = post.comments?.[0]; expect(comment.isDeleted).toBe(false); expect(typeof comment.isDeleted).toBe("boolean"); }); it("should transform nested relation fields in toClient", () => { const view = box.users.createView({ posts: { comments: true }, }); const { toClient } = view.transforms; const dbData = { id: 1, name: "John", posts: [ { id: 10, title: "My Post", isPublished: 1, authorId: 1, comments: [ { id: 100, text: "Comment text", isDeleted: 0, postId: 10, }, ], }, ], }; const clientData = toClient(dbData); expect(clientData.posts[0].isPublished).toBe(true); expect(typeof clientData.posts[0].isPublished).toBe("boolean"); expect(clientData.posts[0].comments[0].isDeleted).toBe(false); expect(typeof clientData.posts[0].comments[0].isDeleted).toBe("boolean"); }); it("should transform nested relation fields in toDb", () => { const view = box.users.createView({ posts: { comments: true }, }); const { toDb } = view.transforms; const clientData = { id: "user-123", name: "John", posts: [ { id: 10, title: "My Post", isPublished: true, authorId: "user-123", comments: [ { id: 100, text: "Comment text", isDeleted: true, postId: 10, }, ], }, ], }; const dbData = toDb(clientData); expect(dbData.posts[0].isPublished).toBe(1); expect(typeof dbData.posts[0].isPublished).toBe("number"); expect(dbData.posts[0].comments[0].isDeleted).toBe(1); expect(typeof dbData.posts[0].comments[0].isDeleted).toBe("number"); }); it("should work with parseFromDb and parseForDb", () => { const view = box.users.createView({ posts: { comments: true }, }); const { parseFromDb, parseForDb } = view.transforms; const dbRow = { id: 1, name: "John", posts: [ { id: 10, title: "My Post", isPublished: 1, authorId: 1, comments: [ { id: 100, text: "Comment text", isDeleted: 0, postId: 10, }, ], }, ], }; const clientResult = parseFromDb(dbRow); expect(clientResult.id).toBe(1); expect(clientResult.posts[0].isPublished).toBe(true); expect(clientResult.posts[0].comments[0].isDeleted).toBe(false); }); }); describe("sqlOnly fields", () => { const users = schema({ _tableName: "users", id: s.sqlite({ type: "int", pk: true }).client({ value: () => "user-123", schema: z.string(), }), name: s.sqlite({ type: "varchar" }).client({ value: "John" }), internalToken: s.sqlite({ type: "varchar", sqlOnly: true }), }); const box = createSchemaBox({ users }, { users: {} }); it("should exclude sqlOnly fields from client schema", () => { expectTypeOf().not.toHaveProperty("internalToken"); const clientKeys = Object.keys(box.users.schemas.clientChecked.shape); expect(clientKeys).not.toContain("internalToken"); }); it("should include sqlOnly fields in sql schema", () => { expectTypeOf().toHaveProperty("internalToken"); const sqlKeys = Object.keys(box.users.schemas.sql.shape); expect(sqlKeys).toContain("internalToken"); }); it("should exclude sqlOnly fields from defaults", () => { const defaults = box.users.defaults; expect(defaults).not.toHaveProperty("internalToken"); }); it("should exclude sqlOnly fields from toClient output", () => { const { toClient } = box.users.transforms; const dbData = { id: 1, name: "John", internalToken: "secret" }; const result = toClient(dbData); expect(result).not.toHaveProperty("internalToken"); }); it("should NOT include sqlOnly fields in toDb output", () => { const { toDb } = box.users.transforms; const clientData = { id: "1", name: "John" }; const result = toDb(clientData); expect(result).not.toHaveProperty("internalToken"); }); }); describe("SQL enum fields", () => { const posts = schema({ _tableName: "posts", id: s.sqlite({ type: "int", pk: true }), status: s.sqlite({ type: "enum", values: ["draft", "published", "archived"], default: "draft", }), nullableStatus: s.sqlite({ type: "enum", values: ["draft", "published"], nullable: true, }), }); const box = createSchemaBox({ posts }, { posts: {} }); it("should validate enum values in sql, client, and server schemas", () => { expect(box.posts.schemas.sql.shape.status.parse("published")).toBe("published"); expect(box.posts.schemas.clientChecked.shape.status.parse("archived")).toBe("archived"); expect(box.posts.schemas.server.shape.status.parse("draft")).toBe("draft"); expect(() => box.posts.schemas.server.shape.status.parse("deleted")).toThrow(); }); it("should use enum defaults and nullable enum defaults", () => { expect(box.posts.defaults.status).toBe("draft"); expect(box.posts.defaults.nullableStatus).toBeNull(); }); it("should parse enum data for db writes", () => { expect(box.posts.transforms.parseForDb({ id: 1, status: "published", nullableStatus: null, })).toEqual({ id: 1, status: "published", nullableStatus: null }); }); }); describe("derive - computed fields", () => { const users = schema({ _tableName: "users", id: s.sqlite({ type: "int", pk: true }), fullName: s.client(""), firstName: s.sqlite({ type: "varchar" }).client({ value: "John" }), lastName: s.sqlite({ type: "varchar" }).client({ value: "Doe" }), }).derive({ forClient: { fullName: (row) => `${row.firstName} ${row.lastName}`, }, }); const box = createSchemaBox({ users }, { users: {} }); it("should add derived fields to defaults", () => { const defaults = box.users.defaults; expect(defaults.fullName).toBe("John Doe"); }); it("should compute derived fields in toClient", () => { const { toClient } = box.users.transforms; const dbData = { id: 1, firstName: "Jane", lastName: "Smith" }; const result = toClient(dbData); expect(result.fullName).toBe("Jane Smith"); }); it("should NOT include derived fields in sql schema", () => { const sqlKeys = Object.keys(box.users.schemas.sql.shape); expect(sqlKeys).not.toContain("fullName"); }); it("should include derived fields in client schema", () => { expectTypeOf().toHaveProperty("fullName"); }); it("should work with multiple derived fields", () => { const products = schema({ _tableName: "products", id: s.sqlite({ type: "int", pk: true }), price: s.sqlite({ type: "int" }).client({ value: 100 }), quantity: s.sqlite({ type: "int" }).client({ value: 5 }), total: s.client(0), formattedPrice: s.client(""), }).derive({ forClient: { total: (row) => row.price * row.quantity, formattedPrice: (row) => `$${row.price}`, }, }); const box = createSchemaBox({ products }, { products: {} }); const defaults = box.products.defaults; expect(defaults.total).toBe(500); expect(defaults.formattedPrice).toBe("$100"); }); it("should track dependencies for forDb derived fields", () => { const contacts = schema({ _tableName: "contacts", id: s.sqlite({ type: "int", pk: true }), firstName: s.sqlite({ type: "varchar" }).client({ value: "John" }), lastName: s.sqlite({ type: "varchar" }).client({ value: "Doe" }), searchName: s.sqlite({ type: "varchar", sqlOnly: true }), }).derive({ forDb: { searchName: (row) => `${row.firstName} ${row.lastName}`.trim().toLowerCase(), }, }); const contactBox = createSchemaBox({ contacts }, { contacts: {} }); expect(contactBox.contacts.deriveDependencies.searchName).toEqual([ "firstName", "lastName", ]); expect(contactBox.contacts.transforms.parseForDb({ id: 1, firstName: "Ada", lastName: "Lovelace", })).toMatchObject({ searchName: "ada lovelace" }); }); it("should allow forDb derived fields on visible DB columns", () => { const contacts = schema({ _tableName: "contacts", id: s.sqlite({ type: "int", pk: true }), firstName: s.sqlite({ type: "varchar" }).client({ value: "John" }), lastName: s.sqlite({ type: "varchar" }).client({ value: "Doe" }), fullName: s.sqlite({ type: "varchar" }).client({ value: "" }), }).derive({ forDb: { fullName: (row) => `${row.firstName} ${row.lastName}`.trim(), }, }); const contactBox = createSchemaBox({ contacts }, { contacts: {} }); expect(contactBox.contacts.transforms.parseForDb({ id: 1, firstName: "Ada", lastName: "Lovelace", fullName: "stale", })).toMatchObject({ fullName: "Ada Lovelace" }); }); }); describe("refine", () => { it("should run server refinement on parseForDb", () => { const events = schema({ _tableName: "events", id: s.sqlite({ type: "int", pk: true }), startDate: s.sqlite({ type: "varchar" }).client({ value: "" }), endDate: s.sqlite({ type: "varchar" }).client({ value: "" }), content: s.sqlite({ type: "varchar", nullable: true }).client({ value: null, schema: z.string().nullable(), }), isPublished: s.sqlite({ type: "boolean" }).client({ value: false }), }).refine((r) => [