From 658cd9818dd99428b28d30f76cb1d186ab90b284 Mon Sep 17 00:00:00 2001 From: Nate Wienert Date: Sat, 4 Jan 2025 20:27:14 -0800 Subject: [PATCH 1/3] fix optional types --- src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index 4d17e07..0699088 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,8 +87,8 @@ type ZeroMappedCustomType< type ZeroColumnDefinition> = Readonly<{ readonly optional: ColumnDefinition["_"]["notNull"] extends true - ? boolean // false - : boolean; // true; + ? false + : true; readonly type: ZeroMappedColumnType; readonly customType: ZeroMappedCustomType; } & ( From 69dc52698fc1be3e7570ca4cdcbcb4e7782b802d Mon Sep 17 00:00:00 2001 From: Nate Wienert Date: Sat, 4 Jan 2025 20:35:00 -0800 Subject: [PATCH 2/3] account for default values --- src/types.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/types.ts b/src/types.ts index 0699088..dad7673 100644 --- a/src/types.ts +++ b/src/types.ts @@ -85,10 +85,12 @@ type ZeroMappedCustomType< ? ColumnDefinition["_"]["$type"] : ZeroTypeToTypescriptType[ZeroMappedColumnType]; -type ZeroColumnDefinition> = Readonly<{ - readonly optional: ColumnDefinition["_"]["notNull"] extends true - ? false - : true; +type ZeroColumnDefinition, CD extends ColumnDefinition['_']> = Readonly<{ + readonly optional: CD['default'] extends void + ? CD['notNull'] extends true + ? false + : true + : true; readonly type: ZeroMappedColumnType; readonly customType: ZeroMappedCustomType; } & ( From f7913a3a33cca6975374d29d7bd12711e109a89b Mon Sep 17 00:00:00 2001 From: Chase Adams Date: Sun, 5 Jan 2025 19:50:59 -0700 Subject: [PATCH 3/3] fix: tests for optional typing --- src/relations.ts | 2 +- src/types.ts | 8 +- tests/compile.test.ts | 58 +- tests/relationships.test.ts | 1015 +++++++++++++---------- tests/tables.test.ts | 1560 +++++++++++++++++++++-------------- tests/utils.ts | 5 +- 6 files changed, 1581 insertions(+), 1067 deletions(-) diff --git a/src/relations.ts b/src/relations.ts index 43d3e49..8f4ffa7 100644 --- a/src/relations.ts +++ b/src/relations.ts @@ -183,7 +183,7 @@ const createZeroSchema = < if (!sourceFieldNames.length || !destFieldNames.length) { throw new Error( - `No source or dest field names found for: ${relation.fieldName}`, + `No relationship found for: ${relation.fieldName} (${relation instanceof One ? "One" : "Many"} from ${tableName} to ${relation.referencedTableName}). Did you forget to define foreign keys?`, ); } diff --git a/src/types.ts b/src/types.ts index 29ec680..acc2970 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,14 +94,10 @@ type ZeroColumnDefinition< CD extends ColumnDefinition["_"] = ColumnDefinition["_"], > = Readonly< { - readonly optional: CD["default"] extends void - ? CD["notNull"] extends true - ? false - : true - : true; + readonly optional: CD extends { notNull: true } ? false : true; readonly type: ZeroMappedColumnType; readonly customType: ZeroMappedCustomType; - } & (ColumnDefinition["_"] extends { columnType: "PgEnumColumn" } + } & (CD extends { columnType: "PgEnumColumn" } ? { readonly kind: "enum" } : {}) >; diff --git a/tests/compile.test.ts b/tests/compile.test.ts index 70f6577..d298218 100644 --- a/tests/compile.test.ts +++ b/tests/compile.test.ts @@ -2,7 +2,7 @@ import { execSync } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { expect, test } from "vitest"; +import { expect, test, describe } from "vitest"; const runZeroBuildSchema = async (testName: string) => { const schemaPath = path.join( @@ -36,37 +36,39 @@ const runZeroBuildSchema = async (testName: string) => { } }; -test("compile - one-to-one", async () => { - const result = await runZeroBuildSchema("one-to-one"); - expect(result.schema.tables.user).toBeTruthy(); -}); +describe.concurrent("compile", () => { + test("compile - one-to-one", async () => { + const result = await runZeroBuildSchema("one-to-one"); + expect(result.schema.tables.user).toBeTruthy(); + }); -test("compile - one-to-one-2", async () => { - const result = await runZeroBuildSchema("one-to-one-2"); - expect(result.schema.tables.user).toBeTruthy(); -}); + test("compile - one-to-one-2", async () => { + const result = await runZeroBuildSchema("one-to-one-2"); + expect(result.schema.tables.user).toBeTruthy(); + }); -test("compile - one-to-one-foreign-key", async () => { - const result = await runZeroBuildSchema("one-to-one-foreign-key"); - expect(result.schema.tables.users).toBeTruthy(); -}); + test("compile - one-to-one-foreign-key", async () => { + const result = await runZeroBuildSchema("one-to-one-foreign-key"); + expect(result.schema.tables.users).toBeTruthy(); + }); -test("compile - one-to-one-self", async () => { - const result = await runZeroBuildSchema("one-to-one-self"); - expect(result.schema.tables.user).toBeTruthy(); -}); + test("compile - one-to-one-self", async () => { + const result = await runZeroBuildSchema("one-to-one-self"); + expect(result.schema.tables.user).toBeTruthy(); + }); -test("compile - one-to-many", async () => { - const result = await runZeroBuildSchema("one-to-many"); - expect(result.schema.tables.user).toBeTruthy(); -}); + test("compile - one-to-many", async () => { + const result = await runZeroBuildSchema("one-to-many"); + expect(result.schema.tables.user).toBeTruthy(); + }); -test("compile - one-to-many-named", async () => { - const result = await runZeroBuildSchema("one-to-many-named"); - expect(result.schema.tables.users).toBeTruthy(); -}); + test("compile - one-to-many-named", async () => { + const result = await runZeroBuildSchema("one-to-many-named"); + expect(result.schema.tables.users).toBeTruthy(); + }); -test("compile - many-to-many", async () => { - const result = await runZeroBuildSchema("many-to-many"); - expect(result.schema.tables.user).toBeTruthy(); + test("compile - many-to-many", async () => { + const result = await runZeroBuildSchema("many-to-many"); + expect(result.schema.tables.user).toBeTruthy(); + }); }); diff --git a/tests/relationships.test.ts b/tests/relationships.test.ts index 15ab2cd..2526203 100644 --- a/tests/relationships.test.ts +++ b/tests/relationships.test.ts @@ -1,5 +1,5 @@ -import { column, createSchema, type JSONValue } from "@rocicorp/zero"; -import { test } from "vitest"; +import { createSchema, type JSONValue } from "@rocicorp/zero"; +import { test, describe } from "vitest"; import { Expect, expectSchemaDeepEqual, @@ -7,437 +7,620 @@ import { type Equal, } from "./utils"; -test("relationships - one-to-one self-referential", async () => { - const { schema: oneToOneSelfZeroSchema } = await import( - "./schemas/one-to-one-self.zero" - ); - - const expectedUsers = { - tableName: "user", - columns: { - id: column.number(), - name: column.string(true), - invited_by: column.number(true), - }, - primaryKey: ["id"], - relationships: { - invitee: { - sourceField: ["invited_by"] as AtLeastOne<"id" | "name" | "invited_by">, - destField: ["id"] as AtLeastOne<"id" | "name" | "invited_by">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expected = createSchema({ - version: 1, - tables: { - user: expectedUsers, - }, +describe.concurrent("relationships", () => { + test("relationships - one-to-one self-referential", async () => { + const { schema: oneToOneSelfZeroSchema } = await import( + "./schemas/one-to-one-self.zero" + ); + + const expectedUsers = { + tableName: "user", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + invited_by: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + }, + primaryKey: ["id"], + relationships: { + invitee: { + sourceField: ["invited_by"] as AtLeastOne< + "id" | "name" | "invited_by" + >, + destField: ["id"] as AtLeastOne<"id" | "name" | "invited_by">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expected = createSchema({ + version: 1, + tables: { + user: expectedUsers, + }, + }); + + expectSchemaDeepEqual(oneToOneSelfZeroSchema).toEqual(expected); + Expect< + Equal< + typeof oneToOneSelfZeroSchema.tables.user.columns.name, + typeof expected.tables.user.columns.name + > + >; }); - expectSchemaDeepEqual(oneToOneSelfZeroSchema).toEqual(expected); - Expect>; -}); + test("relationships - one-to-one", async () => { + const { schema: oneToOneZeroSchema } = await import( + "./schemas/one-to-one.zero" + ); + + const expectedUsers = { + tableName: "user", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + profileInfo: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["user_id"] as AtLeastOne<"id" | "user_id" | "metadata">, + destSchema: () => expectedProfileInfo, + }, + }, + } as const; + + const expectedProfileInfo = { + tableName: "profile_info", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + user_id: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + metadata: { + type: "json", + optional: true, + customType: null as unknown as JSONValue, + }, + }, + primaryKey: ["id"], + relationships: { + user: { + sourceField: ["user_id"] as AtLeastOne<"id" | "user_id" | "metadata">, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expected = createSchema({ + version: 1, + tables: { + user: expectedUsers, + profile_info: expectedProfileInfo, + }, + }); -test("relationships - one-to-one", async () => { - const { schema: oneToOneZeroSchema } = await import( - "./schemas/one-to-one.zero" - ); - - const expectedUsers = { - tableName: "user", - columns: { - id: column.number(), - name: column.string(true), - }, - primaryKey: ["id"], - relationships: { - profileInfo: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["user_id"] as AtLeastOne<"id" | "user_id" | "metadata">, - destSchema: () => expectedProfileInfo, - }, - }, - } as const; - - const expectedProfileInfo = { - tableName: "profile_info", - columns: { - id: column.number(), - user_id: column.number(true), - metadata: column.json(true), - }, - primaryKey: ["id"], - relationships: { - user: { - sourceField: ["user_id"] as AtLeastOne<"id" | "user_id" | "metadata">, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expected = createSchema({ - version: 1, - tables: { - user: expectedUsers, - profile_info: expectedProfileInfo, - }, + expectSchemaDeepEqual(oneToOneZeroSchema).toEqual(expected); + Expect>; }); - expectSchemaDeepEqual(oneToOneZeroSchema).toEqual(expected); - Expect>; -}); + test("relationships - one-to-one-foreign-key", async () => { + const { schema: oneToOneForeignKeyZeroSchema } = await import( + "./schemas/one-to-one-foreign-key.zero" + ); + + const expectedUsers = { + tableName: "users", + columns: { + id: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + userPosts: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["author"] as AtLeastOne<"id" | "name" | "author">, + destSchema: () => expectedPosts, + }, + }, + } as const; + + const expectedPosts = { + tableName: "posts", + columns: { + id: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + author: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + postAuthor: { + sourceField: ["author"] as AtLeastOne<"id" | "name" | "author">, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + }, + } as const; -test("relationships - one-to-one-foreign-key", async () => { - const { schema: oneToOneForeignKeyZeroSchema } = await import( - "./schemas/one-to-one-foreign-key.zero" - ); - - const expectedUsers = { - tableName: "users", - columns: { - id: column.string(), - name: column.string(true), - }, - primaryKey: ["id"], - relationships: { - userPosts: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["author"] as AtLeastOne<"id" | "name" | "author">, - destSchema: () => expectedPosts, - }, - }, - } as const; - - const expectedPosts = { - tableName: "posts", - columns: { - id: column.string(), - name: column.string(true), - author: column.string(), - }, - primaryKey: ["id"], - relationships: { - postAuthor: { - sourceField: ["author"] as AtLeastOne<"id" | "name" | "author">, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expected = createSchema({ - version: 1, - tables: { - users: expectedUsers, - posts: expectedPosts, - }, + const expected = createSchema({ + version: 1, + tables: { + users: expectedUsers, + posts: expectedPosts, + }, + }); + + expectSchemaDeepEqual(oneToOneForeignKeyZeroSchema).toEqual(expected); + Expect>; }); - expectSchemaDeepEqual(oneToOneForeignKeyZeroSchema).toEqual(expected); - Expect>; -}); + test("relationships - one-to-one-2", async () => { + const { schema: oneToOne2ZeroSchema } = await import( + "./schemas/one-to-one-2.zero" + ); + + const expectedUsers = { + tableName: "user", + columns: { + id: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + partner: { + type: "boolean", + optional: false, + customType: null as unknown as boolean, + }, + }, + primaryKey: ["id"], + relationships: { + messages: { + sourceField: ["id"] as AtLeastOne<"id" | "name" | "partner">, + destField: ["senderId"] as AtLeastOne< + "id" | "senderId" | "mediumId" | "body" + >, + destSchema: () => expectedMessage, + }, + }, + } as const; + + const expectedMedium = { + tableName: "medium", + columns: { + id: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + messages: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["mediumId"] as AtLeastOne< + "id" | "mediumId" | "senderId" | "body" + >, + destSchema: () => expectedMessage, + }, + }, + } as const; + + const expectedMessage = { + tableName: "message", + columns: { + id: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + senderId: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + mediumId: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + body: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + medium: { + sourceField: ["mediumId"] as AtLeastOne< + "id" | "mediumId" | "senderId" | "body" + >, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedMedium, + }, + sender: { + sourceField: ["senderId"] as AtLeastOne< + "id" | "senderId" | "mediumId" | "body" + >, + destField: ["id"] as AtLeastOne<"id" | "name" | "partner">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expected = createSchema({ + version: 2.1, + tables: { + user: expectedUsers, + medium: expectedMedium, + message: expectedMessage, + }, + }); -test("relationships - one-to-one-2", async () => { - const { schema: oneToOne2ZeroSchema } = await import( - "./schemas/one-to-one-2.zero" - ); - - const expectedUsers = { - tableName: "user", - columns: { - id: column.string(), - name: column.string(), - partner: column.boolean(), - }, - primaryKey: ["id"], - relationships: { - messages: { - sourceField: ["id"] as AtLeastOne<"id" | "name" | "partner">, - destField: ["senderId"] as AtLeastOne< - "id" | "senderId" | "mediumId" | "body" - >, - destSchema: () => expectedMessage, - }, - }, - } as const; - - const expectedMedium = { - tableName: "medium", - columns: { - id: column.string(), - name: column.string(), - }, - primaryKey: ["id"], - relationships: { - messages: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["mediumId"] as AtLeastOne< - "id" | "mediumId" | "senderId" | "body" - >, - destSchema: () => expectedMessage, - }, - }, - } as const; - - const expectedMessage = { - tableName: "message", - columns: { - id: column.string(), - senderId: column.string(true), - mediumId: column.string(true), - body: column.string(), - }, - primaryKey: ["id"], - relationships: { - medium: { - sourceField: ["mediumId"] as AtLeastOne< - "id" | "mediumId" | "senderId" | "body" - >, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedMedium, - }, - sender: { - sourceField: ["senderId"] as AtLeastOne< - "id" | "senderId" | "mediumId" | "body" - >, - destField: ["id"] as AtLeastOne<"id" | "name" | "partner">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expected = createSchema({ - version: 2.1, - tables: { - user: expectedUsers, - medium: expectedMedium, - message: expectedMessage, - }, + expectSchemaDeepEqual(oneToOne2ZeroSchema).toEqual(expected); + Expect>; }); - expectSchemaDeepEqual(oneToOne2ZeroSchema).toEqual(expected); - Expect>; -}); + test("relationships - one-to-many", async () => { + const { schema: oneToManyZeroSchema } = await import( + "./schemas/one-to-many.zero" + ); + + const expectedUsers = { + tableName: "user", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + posts: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["author_id"] as AtLeastOne< + "id" | "author_id" | "content" + >, + destSchema: () => expectedPosts, + }, + }, + } as const; + + const expectedPosts = { + tableName: "post", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + content: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + author_id: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + }, + primaryKey: ["id"], + relationships: { + author: { + sourceField: ["author_id"] as AtLeastOne< + "id" | "content" | "author_id" + >, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expectedComments = { + tableName: "comment", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + text: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + author_id: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + post_id: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + }, + primaryKey: ["id"], + relationships: { + post: { + sourceField: ["post_id"] as AtLeastOne< + "id" | "text" | "author_id" | "post_id" + >, + destField: ["id"] as AtLeastOne<"id" | "content" | "author_id">, + destSchema: () => expectedPosts, + }, + author: { + sourceField: ["author_id"] as AtLeastOne< + "id" | "text" | "author_id" | "post_id" + >, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expected = createSchema({ + version: 1, + tables: { + user: expectedUsers, + post: expectedPosts, + comment: expectedComments, + }, + }); -test("relationships - one-to-many", async () => { - const { schema: oneToManyZeroSchema } = await import( - "./schemas/one-to-many.zero" - ); - - const expectedUsers = { - tableName: "user", - columns: { - id: column.number(), - name: column.string(true), - }, - primaryKey: ["id"], - relationships: { - posts: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["author_id"] as AtLeastOne<"id" | "author_id" | "content">, - destSchema: () => expectedPosts, - }, - }, - } as const; - - const expectedPosts = { - tableName: "post", - columns: { - id: column.number(), - content: column.string(true), - author_id: column.number(true), - }, - primaryKey: ["id"], - relationships: { - author: { - sourceField: ["author_id"] as AtLeastOne< - "id" | "content" | "author_id" - >, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expectedComments = { - tableName: "comment", - columns: { - id: column.number(), - text: column.string(true), - author_id: column.number(true), - post_id: column.number(true), - }, - primaryKey: ["id"], - relationships: { - post: { - sourceField: ["post_id"] as AtLeastOne< - "id" | "text" | "author_id" | "post_id" - >, - destField: ["id"] as AtLeastOne<"id" | "content" | "author_id">, - destSchema: () => expectedPosts, - }, - author: { - sourceField: ["author_id"] as AtLeastOne< - "id" | "text" | "author_id" | "post_id" - >, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expected = createSchema({ - version: 1, - tables: { - user: expectedUsers, - post: expectedPosts, - comment: expectedComments, - }, + expectSchemaDeepEqual(oneToManyZeroSchema).toEqual(expected); + Expect>; }); - expectSchemaDeepEqual(oneToManyZeroSchema).toEqual(expected); - Expect>; -}); + test("relationships - one-to-many-named", async () => { + const { schema: oneToManyNamedZeroSchema } = await import( + "./schemas/one-to-many-named.zero" + ); + + const expectedUsers = { + tableName: "users", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + author: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["author_id"] as AtLeastOne< + "id" | "author_id" | "content" | "reviewer_id" + >, + destSchema: () => expectedPosts, + }, + reviewer: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["reviewer_id"] as AtLeastOne< + "id" | "reviewer_id" | "content" | "author_id" + >, + destSchema: () => expectedPosts, + }, + }, + } as const; + + const expectedPosts = { + tableName: "posts", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + content: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + author_id: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + reviewer_id: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + }, + primaryKey: ["id"], + relationships: { + author: { + sourceField: ["author_id"] as AtLeastOne< + "id" | "content" | "author_id" | "reviewer_id" + >, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + reviewer: { + sourceField: ["reviewer_id"] as AtLeastOne< + "id" | "content" | "reviewer_id" | "author_id" + >, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expected = createSchema({ + version: 1, + tables: { + users: expectedUsers, + posts: expectedPosts, + }, + }); -test("relationships - one-to-many-named", async () => { - const { schema: oneToManyNamedZeroSchema } = await import( - "./schemas/one-to-many-named.zero" - ); - - const expectedUsers = { - tableName: "users", - columns: { - id: column.number(), - name: column.string(true), - }, - primaryKey: ["id"], - relationships: { - author: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["author_id"] as AtLeastOne< - "id" | "author_id" | "content" | "reviewer_id" - >, - destSchema: () => expectedPosts, - }, - reviewer: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["reviewer_id"] as AtLeastOne< - "id" | "reviewer_id" | "content" | "author_id" - >, - destSchema: () => expectedPosts, - }, - }, - } as const; - - const expectedPosts = { - tableName: "posts", - columns: { - id: column.number(), - content: column.string(true), - author_id: column.number(true), - reviewer_id: column.number(true), - }, - primaryKey: ["id"], - relationships: { - author: { - sourceField: ["author_id"] as AtLeastOne< - "id" | "content" | "author_id" | "reviewer_id" - >, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - reviewer: { - sourceField: ["reviewer_id"] as AtLeastOne< - "id" | "content" | "reviewer_id" | "author_id" - >, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expected = createSchema({ - version: 1, - tables: { - users: expectedUsers, - posts: expectedPosts, - }, + expectSchemaDeepEqual(oneToManyNamedZeroSchema).toEqual(expected); + Expect>; }); - expectSchemaDeepEqual(oneToManyNamedZeroSchema).toEqual(expected); - Expect>; -}); + test("relationships - many-to-many", async () => { + const { schema: manyToManyZeroSchema } = await import( + "./schemas/many-to-many.zero" + ); + + const expectedUsers = { + tableName: "user", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + usersToGroups: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["user_id"] as AtLeastOne<"user_id" | "group_id">, + destSchema: () => expectedUsersToGroups, + }, + }, + } as const; + + const expectedUsersToGroups = { + tableName: "users_to_group", + columns: { + user_id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + group_id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + }, + primaryKey: ["user_id", "group_id"] as Readonly>, + relationships: { + group: { + sourceField: ["group_id"] as AtLeastOne<"user_id" | "group_id">, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedGroups, + }, + user: { + sourceField: ["user_id"] as AtLeastOne<"user_id" | "group_id">, + destField: ["id"] as AtLeastOne<"id" | "name">, + destSchema: () => expectedUsers, + }, + }, + } as const; + + const expectedGroups = { + tableName: "group", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + relationships: { + usersToGroups: { + sourceField: ["id"] as AtLeastOne<"id" | "name">, + destField: ["group_id"] as AtLeastOne<"user_id" | "group_id">, + destSchema: () => expectedUsersToGroups, + }, + }, + } as const; + + const expected = createSchema({ + version: 1, + tables: { + user: expectedUsers, + users_to_group: expectedUsersToGroups, + group: expectedGroups, + }, + }); -test("relationships - many-to-many", async () => { - const { schema: manyToManyZeroSchema } = await import( - "./schemas/many-to-many.zero" - ); - - const expectedUsers = { - tableName: "user", - columns: { - id: column.number(), - name: column.string(true), - }, - primaryKey: ["id"], - relationships: { - usersToGroups: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["user_id"] as AtLeastOne<"user_id" | "group_id">, - destSchema: () => expectedUsersToGroups, - }, - }, - } as const; - - const expectedUsersToGroups = { - tableName: "users_to_group", - columns: { - user_id: column.number(), - group_id: column.number(), - }, - primaryKey: ["user_id", "group_id"] as Readonly>, - relationships: { - group: { - sourceField: ["group_id"] as AtLeastOne<"user_id" | "group_id">, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedGroups, - }, - user: { - sourceField: ["user_id"] as AtLeastOne<"user_id" | "group_id">, - destField: ["id"] as AtLeastOne<"id" | "name">, - destSchema: () => expectedUsers, - }, - }, - } as const; - - const expectedGroups = { - tableName: "group", - columns: { - id: column.number(), - name: column.string(true), - }, - primaryKey: ["id"], - relationships: { - usersToGroups: { - sourceField: ["id"] as AtLeastOne<"id" | "name">, - destField: ["group_id"] as AtLeastOne<"user_id" | "group_id">, - destSchema: () => expectedUsersToGroups, - }, - }, - } as const; - - const expected = createSchema({ - version: 1, - tables: { - user: expectedUsers, - users_to_group: expectedUsersToGroups, - group: expectedGroups, - }, + expectSchemaDeepEqual(manyToManyZeroSchema).toEqual(expected); + Expect>; }); - - expectSchemaDeepEqual(manyToManyZeroSchema).toEqual(expected); - Expect>; }); diff --git a/tests/tables.test.ts b/tests/tables.test.ts index e1d4929..a886ad1 100644 --- a/tests/tables.test.ts +++ b/tests/tables.test.ts @@ -22,7 +22,7 @@ import { uuid, varchar, } from "drizzle-orm/pg-core"; -import { expect, test } from "vitest"; +import { expect, test, describe } from "vitest"; import { createZeroTableSchema, type ColumnsConfig } from "../src"; import { type Equal, @@ -31,691 +31,1023 @@ import { type ZeroTableSchema, } from "./utils"; -test("pg - basic", () => { - const table = pgTable("test", { - id: serial().primaryKey(), - name: text().notNull(), - json: jsonb().notNull(), - }); - - const result = createZeroTableSchema(table, { - id: true, - name: true, - json: true, - }); - - const expected = { - tableName: "test", - columns: { - id: column.number(), - name: column.string(), - json: column.json(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - named fields", () => { - const table = pgTable("test", { - id: serial("custom_id").primaryKey(), - name: text("custom_name").notNull(), - }); +describe.concurrent("tables", () => { + test("pg - basic", () => { + const table = pgTable("test", { + id: serial().primaryKey(), + name: text().notNull(), + json: jsonb().notNull(), + }); - const result = createZeroTableSchema(table, { - custom_id: true, - custom_name: true, + const result = createZeroTableSchema(table, { + id: true, + name: true, + json: true, + }); + + const expected = { + tableName: "test", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + json: { + type: "json", + optional: false, + customType: null as unknown as JSONValue, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const expected = { - tableName: "test", - columns: { - custom_id: column.number(), - custom_name: column.string(), - }, - primaryKey: ["custom_id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - custom types", () => { - const table = pgTable("test", { - id: text().primaryKey(), - json: jsonb().$type<{ foo: string }>().notNull(), + test("pg - named fields", () => { + const table = pgTable("test", { + id: serial("custom_id").primaryKey(), + name: text("custom_name").notNull(), + }); + + const result = createZeroTableSchema(table, { + custom_id: true, + custom_name: true, + }); + + const expected = { + tableName: "test", + columns: { + custom_id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + custom_name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + }, + primaryKey: ["custom_id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: column.string(), - json: true, - }); + test("pg - custom types", () => { + const table = pgTable("test", { + id: text().primaryKey(), + json: jsonb().$type<{ foo: string }>().notNull(), + }); - const expected = { - tableName: "test", - columns: { + const result = createZeroTableSchema(table, { id: column.string(), - json: column.json<{ foo: string }>(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - optional fields", () => { - const table = pgTable("test", { - id: serial().primaryKey(), - name: text(), // optional - description: text(), // optional - metadata: jsonb(), // optional + json: true, + }); + + const expected = { + tableName: "test", + columns: { + id: column.string(), + json: { + type: "json", + optional: false, + customType: null as unknown as { foo: string }, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - name: true, - description: true, - metadata: true, - }); + test("pg - optional fields", () => { + const table = pgTable("test", { + id: serial().primaryKey(), + name: text(), // optional + description: text(), // optional + metadata: jsonb(), // optional + }); - const expected = { - tableName: "test", - columns: { - id: column.number(), - name: column.string(true), - description: column.string(true), - metadata: column.json(true), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - array types", () => { - const table = pgTable("test", { - id: serial().primaryKey(), - tags: text().array().notNull(), - scores: jsonb().array(), - }); - - expect(() => - createZeroTableSchema(table, { + const result = createZeroTableSchema(table, { id: true, - tags: true, - scores: true, - }), - ).toThrow("Unsupported column type: array"); -}); - -test("pg - complex custom types", () => { - type UserMetadata = { - preferences: { - theme: "light" | "dark"; - notifications: boolean; - }; - lastLogin: string; - }; - - const table = pgTable("users", { - id: serial().primaryKey(), - metadata: jsonb().$type().notNull(), - settings: jsonb().$type>(), - }); - - const result = createZeroTableSchema(table, { - id: true, - metadata: true, - settings: true, - }); - - const expected = { - tableName: "users", - columns: { - id: column.number(), - metadata: column.json(), - settings: column.json>(true), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - partial column selection", () => { - const table = pgTable("test", { - id: serial().primaryKey(), - name: text().notNull(), - age: serial().notNull(), - metadata: jsonb().notNull(), + name: true, + description: true, + metadata: true, + }); + + const expected = { + tableName: "test", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + description: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + metadata: { + type: "json", + optional: true, + customType: null as unknown as JSONValue, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - metadata: true, - name: false, - age: false, + test("pg - array types", () => { + const table = pgTable("test", { + id: serial().primaryKey(), + tags: text().array().notNull(), + scores: jsonb().array(), + }); + + expect(() => + createZeroTableSchema(table, { + id: true, + tags: true, + scores: true, + }), + ).toThrow("Unsupported column type: array"); }); - const expected = { - tableName: "test", - columns: { - id: column.number(), - metadata: column.json(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + test("pg - complex custom types", () => { + type UserMetadata = { + preferences: { + theme: "light" | "dark"; + notifications: boolean; + }; + lastLogin: string; + }; -test("pg - partial column selection", () => { - const table = pgTable("test", { - id: serial().primaryKey(), - name: text().notNull(), - age: serial().notNull(), - metadata: jsonb().notNull(), - }); + const table = pgTable("users", { + id: serial().primaryKey(), + metadata: jsonb().$type().notNull(), + settings: jsonb().$type>(), + }); - const result = createZeroTableSchema(table, { - id: true, - metadata: true, - name: false, - age: false, + const result = createZeroTableSchema(table, { + id: true, + metadata: true, + settings: true, + }); + + const expected = { + tableName: "users", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + metadata: { + type: "json", + optional: false, + customType: null as unknown as UserMetadata, + }, + settings: { + type: "json", + optional: true, + customType: null as unknown as Record, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const expected = { - tableName: "test", - columns: { - id: column.number(), - metadata: column.json(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + test("pg - partial column selection", () => { + const table = pgTable("test", { + id: serial().primaryKey(), + name: text().notNull(), + age: serial().notNull(), + metadata: jsonb().notNull(), + }); -test("pg - composite primary key", () => { - const table = pgTable( - "composite_test", - { - userId: text().notNull(), - orgId: text().notNull(), - role: text().notNull(), - }, - (t) => [primaryKey({ columns: [t.userId, t.orgId] })], - ); - - const result = createZeroTableSchema(table, { - userId: true, - orgId: true, - role: true, + const result = createZeroTableSchema(table, { + id: true, + metadata: true, + name: false, + age: false, + }); + + const expected = { + tableName: "test", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + metadata: { + type: "json", + optional: false, + customType: null as unknown as JSONValue, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const expected = { - tableName: "composite_test", - columns: { - userId: column.string(), - orgId: column.string(), - role: column.string(), - }, - // this type is erased in drizzle, so we need to cast it - primaryKey: ["userId", "orgId"] as readonly [string, ...string[]], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + test("pg - partial column selection", () => { + const table = pgTable("test", { + id: serial().primaryKey(), + name: text().notNull(), + age: serial().notNull(), + metadata: jsonb().notNull(), + }); -test("pg - timestamp fields", () => { - const table = pgTable("events", { - id: serial().primaryKey(), - createdAt: timestamp().notNull().defaultNow(), - updatedAt: timestamp(), - scheduledFor: timestamp().notNull(), + const result = createZeroTableSchema(table, { + id: true, + metadata: true, + name: false, + age: false, + }); + + const expected = { + tableName: "test", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + metadata: { + type: "json", + optional: false, + customType: null as unknown as JSONValue, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - createdAt: true, - updatedAt: true, - scheduledFor: true, + test("pg - composite primary key", () => { + const table = pgTable( + "composite_test", + { + userId: text().notNull(), + orgId: text().notNull(), + role: text().notNull(), + }, + (t) => [primaryKey({ columns: [t.userId, t.orgId] })], + ); + + const result = createZeroTableSchema(table, { + userId: true, + orgId: true, + role: true, + }); + + const expected = { + tableName: "composite_test", + columns: { + userId: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + orgId: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + role: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + }, + // this type is erased in drizzle, so we need to cast it + primaryKey: ["userId", "orgId"] as readonly [string, ...string[]], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const expected = { - tableName: "events", - columns: { - id: column.number(), - createdAt: column.string(), - updatedAt: column.string(true), - scheduledFor: column.string(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + test("pg - timestamp fields", () => { + const table = pgTable("events", { + id: serial().primaryKey(), + createdAt: timestamp().notNull().defaultNow(), + updatedAt: timestamp(), + scheduledFor: timestamp().notNull(), + }); -test("pg - custom column mapping", () => { - const table = pgTable("users", { - id: serial().primaryKey(), - firstName: text("first_name").notNull(), - lastName: text("last_name").notNull(), - profileData: jsonb("profile_data").$type<{ - bio: string; - avatar: string; - }>(), - }); - - const result = createZeroTableSchema(table, { - id: true, - first_name: true, - last_name: true, - profile_data: true, + const result = createZeroTableSchema(table, { + id: true, + createdAt: true, + updatedAt: true, + scheduledFor: true, + }); + + const expected = { + tableName: "events", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + createdAt: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + updatedAt: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + scheduledFor: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - // result. - - const expected = { - tableName: "users", - columns: { - id: column.number(), - first_name: column.string(), - last_name: column.string(), - profile_data: column.json<{ + test("pg - custom column mapping", () => { + const table = pgTable("users", { + id: serial().primaryKey(), + firstName: text("first_name").notNull(), + lastName: text("last_name").notNull(), + profileData: jsonb("profile_data").$type<{ bio: string; avatar: string; - }>(true), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; + }>(), + }); - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - enum field", () => { - const roleEnum = pgEnum("user_role", ["admin", "user", "guest"]); - - const table = pgTable("users", { - id: serial().primaryKey(), - role: roleEnum().notNull(), - backupRole: roleEnum(), - }); - - const result = createZeroTableSchema(table, { - id: true, - role: true, - backupRole: true, + const result = createZeroTableSchema(table, { + id: true, + first_name: true, + last_name: true, + profile_data: true, + }); + + // result. + + const expected = { + tableName: "users", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + first_name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + last_name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + profile_data: { + type: "json", + optional: true, + customType: null as unknown as { + bio: string; + avatar: string; + }, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const expected = { - tableName: "users", - columns: { - id: column.number(), - role: column.enumeration<"admin" | "user" | "guest">(), - backupRole: column.enumeration<"admin" | "user" | "guest">(true), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + test("pg - enum field", () => { + const roleEnum = pgEnum("user_role", ["admin", "user", "guest"]); -test("pg - simple enum field", () => { - const moodEnum = pgEnum("mood_type", ["happy", "sad", "ok"]); + const table = pgTable("users", { + id: serial().primaryKey(), + role: roleEnum().notNull(), + backupRole: roleEnum(), + }); - const table = pgTable("users", { - id: text().primaryKey(), - name: text().notNull(), - mood: moodEnum().notNull(), + const result = createZeroTableSchema(table, { + id: true, + role: true, + backupRole: true, + }); + + const expected = { + tableName: "users", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + role: { + type: "string", + optional: false, + customType: null as unknown as "admin" | "user" | "guest", + kind: "enum", + }, + backupRole: { + type: "string", + optional: true, + customType: null as unknown as "admin" | "user" | "guest", + kind: "enum", + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - name: true, - mood: true, - }); + test("pg - simple enum field", () => { + const moodEnum = pgEnum("mood_type", ["happy", "sad", "ok"]); - const expected = { - tableName: "users", - columns: { - id: column.string(), - name: column.string(), - mood: column.enumeration<"happy" | "sad" | "ok">(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + const table = pgTable("users", { + id: text().primaryKey(), + name: text().notNull(), + mood: moodEnum().notNull(), + }); -test("pg - all supported data types", () => { - const statusEnum = pgEnum("status_type", ["active", "inactive", "pending"]); - - const table = pgTable("all_types", { - // Integer types - id: serial("id").primaryKey(), - smallint: smallint("smallint").notNull(), - integer: integer("integer").notNull(), - bigint: bigint("bigint", { mode: "number" }).notNull(), - - // Serial types - smallSerial: smallserial("smallserial").notNull(), - regularSerial: serial("regular_serial").notNull(), - bigSerial: bigserial("bigserial", { mode: "number" }).notNull(), - - // Arbitrary precision types - numeric: numeric("numeric", { precision: 10, scale: 2 }).notNull(), - decimal: numeric("decimal", { precision: 10, scale: 2 }).notNull(), - - // Floating-point types - real: real("real").notNull(), - doublePrecision: doublePrecision("double_precision").notNull(), - - // String types - name: text().notNull(), - code: char().notNull(), - identifier: uuid().notNull(), - description: varchar().notNull(), - isActive: boolean().notNull(), - createdAt: timestamp().notNull(), - updatedAt: timestamp({ withTimezone: true }).notNull(), - birthDate: date().notNull(), - metadata: jsonb().notNull(), - settings: json().$type<{ theme: string; fontSize: number }>().notNull(), - status: statusEnum().notNull(), - - // Optional variants - optionalSmallint: smallint("optional_smallint"), - optionalInteger: integer("optional_integer"), - optionalBigint: bigint("optional_bigint", { mode: "number" }), - optionalNumeric: numeric("optional_numeric", { precision: 10, scale: 2 }), - optionalReal: real("optional_real"), - optionalDoublePrecision: doublePrecision("optional_double_precision"), - optionalText: text("optional_text"), - optionalBoolean: boolean("optional_boolean"), - optionalDate: timestamp("optional_date"), - optionalJson: jsonb("optional_json"), - optionalEnum: statusEnum("optional_enum"), + const result = createZeroTableSchema(table, { + id: true, + name: true, + mood: true, + }); + + const expected = { + tableName: "users", + columns: { + id: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + mood: { + type: "string", + optional: false, + customType: null as unknown as "happy" | "sad" | "ok", + kind: "enum", + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - smallint: true, - integer: true, - bigint: true, - smallserial: true, - regular_serial: true, - bigserial: true, - numeric: true, - decimal: true, - real: true, - double_precision: true, - name: true, - code: true, - identifier: true, - description: true, - isActive: true, - createdAt: true, - updatedAt: true, - birthDate: true, - metadata: true, - settings: true, - status: true, - optional_smallint: true, - optional_integer: true, - optional_bigint: true, - optional_numeric: true, - optional_real: true, - optional_double_precision: true, - optional_text: true, - optional_boolean: true, - optional_date: true, - optional_json: true, - optional_enum: true, - }); + test("pg - all supported data types", () => { + const statusEnum = pgEnum("status_type", ["active", "inactive", "pending"]); - const expected = { - tableName: "all_types", - columns: { + const table = pgTable("all_types", { // Integer types - id: column.number(), - smallint: column.number(), - integer: column.number(), - bigint: column.number(), + id: serial("id").primaryKey(), + smallint: smallint("smallint").notNull(), + integer: integer("integer").notNull(), + bigint: bigint("bigint", { mode: "number" }).notNull(), // Serial types - smallserial: column.number(), - regular_serial: column.number(), - bigserial: column.number(), + smallSerial: smallserial("smallserial").notNull(), + regularSerial: serial("regular_serial").notNull(), + bigSerial: bigserial("bigserial", { mode: "number" }).notNull(), // Arbitrary precision types - numeric: column.number(), - decimal: column.number(), + numeric: numeric("numeric", { precision: 10, scale: 2 }).notNull(), + decimal: numeric("decimal", { precision: 10, scale: 2 }).notNull(), // Floating-point types - real: column.number(), - double_precision: column.number(), - - // Rest of the types - name: column.string(), - code: column.string(), - identifier: column.string(), - description: column.string(), - isActive: column.boolean(), - createdAt: column.string(), - updatedAt: column.string(), - birthDate: column.string(), - metadata: column.json(), - settings: column.json<{ theme: string; fontSize: number }>(), - status: column.enumeration<"active" | "inactive" | "pending">(), + real: real("real").notNull(), + doublePrecision: doublePrecision("double_precision").notNull(), + + // String types + name: text().notNull(), + code: char().notNull(), + identifier: uuid().notNull(), + description: varchar().notNull(), + isActive: boolean().notNull(), + createdAt: timestamp().notNull(), + updatedAt: timestamp({ withTimezone: true }).notNull(), + birthDate: date().notNull(), + metadata: jsonb().notNull(), + settings: json().$type<{ theme: string; fontSize: number }>().notNull(), + status: statusEnum().notNull(), // Optional variants - optional_smallint: column.number(true), - optional_integer: column.number(true), - optional_bigint: column.number(true), - optional_numeric: column.number(true), - optional_real: column.number(true), - optional_double_precision: column.number(true), - optional_text: column.string(true), - optional_boolean: column.boolean(true), - optional_date: column.string(true), - optional_json: column.json(true), - optional_enum: column.enumeration<"active" | "inactive" | "pending">( - true, - ), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - override column json type", () => { - const table = pgTable("metrics", { - id: serial().primaryKey(), - metadata: jsonb().notNull(), + optionalSmallint: smallint("optional_smallint"), + optionalInteger: integer("optional_integer"), + optionalBigint: bigint("optional_bigint", { mode: "number" }), + optionalNumeric: numeric("optional_numeric", { precision: 10, scale: 2 }), + optionalReal: real("optional_real"), + optionalDoublePrecision: doublePrecision("optional_double_precision"), + optionalText: text("optional_text"), + optionalBoolean: boolean("optional_boolean"), + optionalDate: timestamp("optional_date"), + optionalJson: jsonb("optional_json"), + optionalEnum: statusEnum("optional_enum"), + }); + + const result = createZeroTableSchema(table, { + id: true, + smallint: true, + integer: true, + bigint: true, + smallserial: true, + regular_serial: true, + bigserial: true, + numeric: true, + decimal: true, + real: true, + double_precision: true, + name: true, + code: true, + identifier: true, + description: true, + isActive: true, + createdAt: true, + updatedAt: true, + birthDate: true, + metadata: true, + settings: true, + status: true, + optional_smallint: true, + optional_integer: true, + optional_bigint: true, + optional_numeric: true, + optional_real: true, + optional_double_precision: true, + optional_text: true, + optional_boolean: true, + optional_date: true, + optional_json: true, + optional_enum: true, + }); + + const expected = { + tableName: "all_types", + columns: { + // Integer types + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + smallint: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + integer: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + bigint: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + + // Serial types + smallserial: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + regular_serial: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + bigserial: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + + // Arbitrary precision types + numeric: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + decimal: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + + // Floating-point types + real: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + double_precision: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + + // Rest of the types + name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + code: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + identifier: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + description: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + isActive: { + type: "boolean", + optional: false, + customType: null as unknown as boolean, + }, + createdAt: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + updatedAt: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + birthDate: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + metadata: { + type: "json", + optional: false, + customType: null as unknown as JSONValue, + }, + settings: { + type: "json", + optional: false, + customType: null as unknown as { theme: string; fontSize: number }, + }, + status: { + type: "string", + optional: false, + customType: null as unknown as "active" | "inactive" | "pending", + kind: "enum", + }, + + // Optional variants + optional_smallint: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + optional_integer: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + optional_bigint: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + optional_numeric: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + optional_real: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + optional_double_precision: { + type: "number", + optional: true, + customType: null as unknown as number, + }, + optional_text: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + optional_boolean: { + type: "boolean", + optional: true, + customType: null as unknown as boolean, + }, + optional_date: { + type: "string", + optional: true, + customType: null as unknown as string, + }, + optional_json: { + type: "json", + optional: true, + customType: null as unknown as JSONValue, + }, + optional_enum: { + type: "string", + optional: true, + customType: null as unknown as "active" | "inactive" | "pending", + kind: "enum", + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - metadata: column.json<{ amount: number; currency: string }>(), - }); + test("pg - override column json type", () => { + const table = pgTable("metrics", { + id: serial().primaryKey(), + metadata: jsonb().notNull(), + }); - const expected = { - tableName: "metrics", - columns: { - id: column.number(), + const result = createZeroTableSchema(table, { + id: true, metadata: column.json<{ amount: number; currency: string }>(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - compound primary key with serial", () => { - const table = pgTable( - "order_items", - { - orderId: serial().notNull(), - productId: text().notNull(), - quantity: integer().notNull(), - price: numeric().notNull(), - }, - (t) => [primaryKey({ columns: [t.orderId, t.productId] })], - ); - - const result = createZeroTableSchema(table, { - orderId: true, - productId: true, - quantity: true, - price: true, - }); - - const expected = { - tableName: "order_items", - columns: { - orderId: column.number(), - productId: column.string(), - quantity: column.number(), - price: column.number(), - }, - primaryKey: ["orderId", "productId"] as readonly [string, ...string[]], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - default values", () => { - const table = pgTable("items", { - id: serial().primaryKey(), - name: text().notNull().default("unnamed"), - isActive: boolean().notNull().default(true), - score: integer().notNull().default(0), + }); + + const expected = { + tableName: "metrics", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + metadata: column.json<{ amount: number; currency: string }>(), + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - name: true, - isActive: true, - score: true, + test("pg - compound primary key with serial", () => { + const table = pgTable( + "order_items", + { + orderId: serial().notNull(), + productId: text().notNull(), + quantity: integer().notNull(), + price: numeric().notNull(), + }, + (t) => [primaryKey({ columns: [t.orderId, t.productId] })], + ); + + const result = createZeroTableSchema(table, { + orderId: true, + productId: true, + quantity: true, + price: true, + }); + + const expected = { + tableName: "order_items", + columns: { + orderId: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + productId: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + quantity: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + price: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + }, + primaryKey: ["orderId", "productId"] as readonly [string, ...string[]], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const expected = { - tableName: "items", - columns: { - id: column.number(), - name: column.string(), - isActive: column.boolean(), - score: column.number(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + test("pg - default values", () => { + const table = pgTable("items", { + id: serial().primaryKey(), + name: text().notNull().default("unnamed"), + isActive: boolean().notNull().default(true), + score: integer().notNull().default(0), + }); -test("pg - mixed required and optional json fields", () => { - type ComplexMetadata = { - required: { - version: number; - features: string[]; - }; - optional?: { - preferences: Record; - lastAccessed?: string; - }; - }; - - const table = pgTable("configs", { - id: serial().primaryKey(), - requiredJson: jsonb().$type<{ key: string }>().notNull(), - optionalJson: jsonb().$type(), - mixedJson: json() - .$type<{ required: number; optional?: string }>() - .notNull(), + const result = createZeroTableSchema(table, { + id: true, + name: true, + isActive: true, + score: true, + }); + + const expected = { + tableName: "items", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: { + type: "string", + optional: false, + customType: null as unknown as string, + }, + isActive: { + type: "boolean", + optional: false, + customType: null as unknown as boolean, + }, + score: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - requiredJson: true, - optionalJson: true, - mixedJson: true, - }); + test("pg - mixed required and optional json fields", () => { + type ComplexMetadata = { + required: { + version: number; + features: string[]; + }; + optional?: { + preferences: Record; + lastAccessed?: string; + }; + }; - const expected = { - tableName: "configs", - columns: { - id: column.number(), - requiredJson: column.json<{ key: string }>(), - optionalJson: column.json(true), - mixedJson: column.json<{ required: number; optional?: string }>(), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); + const table = pgTable("configs", { + id: serial().primaryKey(), + requiredJson: jsonb().$type<{ key: string }>().notNull(), + optionalJson: jsonb().$type(), + mixedJson: json() + .$type<{ required: number; optional?: string }>() + .notNull(), + }); -test("pg - custom column selection with overrides", () => { - const table = pgTable("products", { - id: serial().primaryKey(), - name: text().notNull(), - description: text(), - metadata: jsonb().$type>(), + const result = createZeroTableSchema(table, { + id: true, + requiredJson: true, + optionalJson: true, + mixedJson: true, + }); + + const expected = { + tableName: "configs", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + requiredJson: { + type: "json", + optional: false, + customType: null as unknown as { key: string }, + }, + optionalJson: { + type: "json", + optional: true, + customType: null as unknown as ComplexMetadata, + }, + mixedJson: { + type: "json", + optional: false, + customType: null as unknown as { + required: number; + optional?: string; + }, + }, + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - const result = createZeroTableSchema(table, { - id: true, - name: column.string(true), - description: column.string(), - metadata: column.json<{ category: string; tags: string[] }>(true), - }); + test("pg - custom column selection with type overrides", () => { + const table = pgTable("products", { + id: serial().primaryKey(), + name: text().notNull(), + description: text(), + metadata: jsonb().$type>(), + }); - const expected = { - tableName: "products", - columns: { - id: column.number(), + const result = createZeroTableSchema(table, { + id: true, name: column.string(true), description: column.string(), metadata: column.json<{ category: string; tags: string[] }>(true), - }, - primaryKey: ["id"], - } as const satisfies ZeroTableSchema; - - expectTableSchemaDeepEqual(result).toEqual(expected); - Expect>; -}); - -test("pg - invalid column selection", () => { - const table = pgTable("test", { - id: serial().primaryKey(), - name: text().notNull(), - nonexistent: text(), + }); + + const expected = { + tableName: "products", + columns: { + id: { + type: "number", + optional: false, + customType: null as unknown as number, + }, + name: column.string(true), + description: column.string(), + metadata: column.json<{ category: string; tags: string[] }>(true), + }, + primaryKey: ["id"], + } as const satisfies ZeroTableSchema; + + expectTableSchemaDeepEqual(result).toEqual(expected); + Expect>; }); - expect(() => - createZeroTableSchema(table, { - id: true, - name: true, - } as unknown as ColumnsConfig), - ).toThrow(); + test("pg - invalid column selection", () => { + const table = pgTable("test", { + id: serial().primaryKey(), + name: text().notNull(), + nonexistent: text(), + }); + + expect(() => + createZeroTableSchema(table, { + id: true, + name: true, + } as unknown as ColumnsConfig), + ).toThrow(); + }); }); diff --git a/tests/utils.ts b/tests/utils.ts index dcbdde9..6d5b494 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -9,8 +9,9 @@ export function expectTableSchemaDeepEqual( ) { return { toEqual(expected: S, depth = 0) { - if (depth > 8) { - console.debug("reached relationship depth > 8"); + if (depth > 10) { + // the comparison is only checking the first 10 levels of relationships + // to avoid infinite loops return; }