@zeno-lib/schema 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/src/index.test-d.ts +54 -0
- package/src/index.test.ts +119 -0
- package/src/index.ts +135 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zeno-lib/schema",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"url": "https://github.com/zeno-lib/zeno.git",
|
|
16
|
+
"directory": "packages/schema"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "vitest --run",
|
|
20
|
+
"test:watch": "vitest watch",
|
|
21
|
+
"types:check": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"drizzle-orm": "1.0.0-rc.3",
|
|
25
|
+
"zod": ">=4"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@zeno-lib/typescript": "workspace:^",
|
|
29
|
+
"drizzle-orm": "1.0.0-rc.3",
|
|
30
|
+
"typescript": "6.0.3",
|
|
31
|
+
"vitest": "4.1.0",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { integer, pgTable, text } from "drizzle-orm/pg-core"
|
|
2
|
+
import { expectTypeOf, test } from "vitest"
|
|
3
|
+
import { z } from "zod"
|
|
4
|
+
|
|
5
|
+
import { defineTableSchema } from "./index"
|
|
6
|
+
|
|
7
|
+
type StandardSchema<T> = {
|
|
8
|
+
readonly "~standard": {
|
|
9
|
+
readonly types?: { readonly input: T; readonly output: T }
|
|
10
|
+
readonly validate: (value: unknown) => unknown
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function acceptsFormsSchema<TFormData>(
|
|
15
|
+
_schema: StandardSchema<TFormData>
|
|
16
|
+
): void {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const posts = pgTable("posts", {
|
|
21
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
22
|
+
slug: text("slug").notNull(),
|
|
23
|
+
title: text("title").notNull(),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test("insert and update schemas infer form-compatible Standard Schema data", () => {
|
|
27
|
+
const schemas = defineTableSchema(posts)
|
|
28
|
+
|
|
29
|
+
acceptsFormsSchema(schemas.insert)
|
|
30
|
+
acceptsFormsSchema(schemas.update)
|
|
31
|
+
|
|
32
|
+
expectTypeOf<z.infer<typeof schemas.insert>>().toEqualTypeOf<{
|
|
33
|
+
slug: string
|
|
34
|
+
title: string
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
expectTypeOf<z.infer<typeof schemas.update>>().toEqualTypeOf<{
|
|
38
|
+
slug?: string | undefined
|
|
39
|
+
title?: string | undefined
|
|
40
|
+
}>()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("refined schemas preserve refinement-specific output types", () => {
|
|
44
|
+
const schemas = defineTableSchema(posts, {
|
|
45
|
+
insert: {
|
|
46
|
+
title: z.literal("fixed"),
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expectTypeOf<z.infer<typeof schemas.insert>>().toEqualTypeOf<{
|
|
51
|
+
slug: string
|
|
52
|
+
title: "fixed"
|
|
53
|
+
}>()
|
|
54
|
+
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
|
|
4
|
+
import { defineTableSchema } from "./index"
|
|
5
|
+
|
|
6
|
+
const posts = pgTable("posts", {
|
|
7
|
+
createdAt: timestamp("created_at", { mode: "date" }).notNull().defaultNow(),
|
|
8
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
9
|
+
slug: text("slug").notNull(),
|
|
10
|
+
summary: text("summary"),
|
|
11
|
+
title: text("title").notNull(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe("defineTableSchema", () => {
|
|
15
|
+
it("returns select, insert, and update Zod schemas for a Drizzle table", () => {
|
|
16
|
+
const schemas = defineTableSchema(posts)
|
|
17
|
+
|
|
18
|
+
expect(
|
|
19
|
+
schemas.select.parse({
|
|
20
|
+
createdAt: new Date("2026-06-08T00:00:00.000Z"),
|
|
21
|
+
id: 1,
|
|
22
|
+
slug: "hello-world",
|
|
23
|
+
summary: null,
|
|
24
|
+
title: "Hello world",
|
|
25
|
+
})
|
|
26
|
+
).toMatchObject({
|
|
27
|
+
id: 1,
|
|
28
|
+
slug: "hello-world",
|
|
29
|
+
summary: null,
|
|
30
|
+
title: "Hello world",
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(
|
|
34
|
+
schemas.insert.parse({
|
|
35
|
+
slug: "hello-world",
|
|
36
|
+
title: "Hello world",
|
|
37
|
+
})
|
|
38
|
+
).toEqual({
|
|
39
|
+
slug: "hello-world",
|
|
40
|
+
title: "Hello world",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
schemas.update.parse({
|
|
45
|
+
title: "Updated title",
|
|
46
|
+
})
|
|
47
|
+
).toEqual({
|
|
48
|
+
title: "Updated title",
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("omits generated columns from insert and update schemas", () => {
|
|
53
|
+
const schemas = defineTableSchema(posts)
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
schemas.insert.parse({
|
|
57
|
+
id: 1,
|
|
58
|
+
slug: "hello-world",
|
|
59
|
+
title: "Hello world",
|
|
60
|
+
})
|
|
61
|
+
).toEqual({
|
|
62
|
+
slug: "hello-world",
|
|
63
|
+
title: "Hello world",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(
|
|
67
|
+
schemas.update.parse({
|
|
68
|
+
id: 1,
|
|
69
|
+
title: "Updated title",
|
|
70
|
+
})
|
|
71
|
+
).toEqual({
|
|
72
|
+
title: "Updated title",
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("applies per-variant refinements only to the targeted schema", () => {
|
|
77
|
+
const schemas = defineTableSchema(posts, {
|
|
78
|
+
insert: {
|
|
79
|
+
title: (schema) => schema.min(3),
|
|
80
|
+
},
|
|
81
|
+
select: {
|
|
82
|
+
slug: (schema) => schema.startsWith("post-"),
|
|
83
|
+
},
|
|
84
|
+
update: {
|
|
85
|
+
title: (schema) => schema.min(10),
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
schemas.insert.safeParse({
|
|
91
|
+
slug: "hello-world",
|
|
92
|
+
title: "Hi",
|
|
93
|
+
}).success
|
|
94
|
+
).toBe(false)
|
|
95
|
+
|
|
96
|
+
expect(
|
|
97
|
+
schemas.update.safeParse({
|
|
98
|
+
title: "Short",
|
|
99
|
+
}).success
|
|
100
|
+
).toBe(false)
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
schemas.select.safeParse({
|
|
104
|
+
createdAt: new Date("2026-06-08T00:00:00.000Z"),
|
|
105
|
+
id: 1,
|
|
106
|
+
slug: "hello-world",
|
|
107
|
+
summary: null,
|
|
108
|
+
title: "Hello world",
|
|
109
|
+
}).success
|
|
110
|
+
).toBe(false)
|
|
111
|
+
|
|
112
|
+
expect(
|
|
113
|
+
schemas.insert.safeParse({
|
|
114
|
+
slug: "hello-world",
|
|
115
|
+
title: "Okay",
|
|
116
|
+
}).success
|
|
117
|
+
).toBe(true)
|
|
118
|
+
})
|
|
119
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* biome-ignore-all lint/performance/noBarrelFile: package root intentionally re-exports the public schema surface. */
|
|
2
|
+
import type { InferInsertModel, InferSelectModel, Table } from "drizzle-orm"
|
|
3
|
+
import {
|
|
4
|
+
type BuildRefine,
|
|
5
|
+
type BuildSchema,
|
|
6
|
+
type CoerceOptions,
|
|
7
|
+
type CreateSchemaFactoryOptions,
|
|
8
|
+
createSchemaFactory as createDrizzleSchemaFactory,
|
|
9
|
+
type NoUnknownKeys,
|
|
10
|
+
} from "drizzle-orm/zod"
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
createInsertSchema,
|
|
14
|
+
createSchemaFactory,
|
|
15
|
+
createSelectSchema,
|
|
16
|
+
createUpdateSchema,
|
|
17
|
+
} from "drizzle-orm/zod"
|
|
18
|
+
|
|
19
|
+
declare const noTableSchemaOptions: unique symbol
|
|
20
|
+
type NoTableSchemaOptions = { [noTableSchemaOptions]?: never }
|
|
21
|
+
|
|
22
|
+
type TableColumns<TTable extends Table> = TTable["_"]["columns"]
|
|
23
|
+
type InsertColumns<TTable extends Table> = Pick<
|
|
24
|
+
TableColumns<TTable>,
|
|
25
|
+
keyof InferInsertModel<TTable>
|
|
26
|
+
>
|
|
27
|
+
|
|
28
|
+
type SelectRefine<
|
|
29
|
+
TTable extends Table,
|
|
30
|
+
TCoerce extends CoerceOptions,
|
|
31
|
+
> = BuildRefine<TableColumns<TTable>, TCoerce>
|
|
32
|
+
type InsertRefine<
|
|
33
|
+
TTable extends Table,
|
|
34
|
+
TCoerce extends CoerceOptions,
|
|
35
|
+
> = BuildRefine<InsertColumns<TTable>, TCoerce>
|
|
36
|
+
type UpdateRefine<
|
|
37
|
+
TTable extends Table,
|
|
38
|
+
TCoerce extends CoerceOptions,
|
|
39
|
+
> = BuildRefine<InsertColumns<TTable>, TCoerce>
|
|
40
|
+
|
|
41
|
+
type DefineTableSchemaOptions<
|
|
42
|
+
TTable extends Table,
|
|
43
|
+
TCoerce extends CoerceOptions = undefined,
|
|
44
|
+
> = {
|
|
45
|
+
factory?: CreateSchemaFactoryOptions<TCoerce>
|
|
46
|
+
insert?: NoUnknownKeys<
|
|
47
|
+
InsertRefine<TTable, TCoerce>,
|
|
48
|
+
InferInsertModel<TTable>
|
|
49
|
+
>
|
|
50
|
+
select?: NoUnknownKeys<
|
|
51
|
+
SelectRefine<TTable, TCoerce>,
|
|
52
|
+
InferSelectModel<TTable>
|
|
53
|
+
>
|
|
54
|
+
update?: UpdateRefine<TTable, TCoerce>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type RefineFromOptions<
|
|
58
|
+
TOptions,
|
|
59
|
+
TKey extends string,
|
|
60
|
+
> = TKey extends keyof TOptions
|
|
61
|
+
? NonNullable<TOptions[TKey]> extends Record<string, unknown>
|
|
62
|
+
? NonNullable<TOptions[TKey]>
|
|
63
|
+
: undefined
|
|
64
|
+
: undefined
|
|
65
|
+
|
|
66
|
+
type DefineTableSchemaResult<
|
|
67
|
+
TTable extends Table,
|
|
68
|
+
TCoerce extends CoerceOptions,
|
|
69
|
+
TOptions,
|
|
70
|
+
> = {
|
|
71
|
+
insert: BuildSchema<
|
|
72
|
+
"insert",
|
|
73
|
+
TableColumns<TTable>,
|
|
74
|
+
RefineFromOptions<TOptions, "insert">,
|
|
75
|
+
TCoerce
|
|
76
|
+
>
|
|
77
|
+
select: BuildSchema<
|
|
78
|
+
"select",
|
|
79
|
+
TableColumns<TTable>,
|
|
80
|
+
RefineFromOptions<TOptions, "select">,
|
|
81
|
+
TCoerce
|
|
82
|
+
>
|
|
83
|
+
update: BuildSchema<
|
|
84
|
+
"update",
|
|
85
|
+
TableColumns<TTable>,
|
|
86
|
+
RefineFromOptions<TOptions, "update">,
|
|
87
|
+
TCoerce
|
|
88
|
+
>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function defineTableSchema<TTable extends Table>(
|
|
92
|
+
table: TTable
|
|
93
|
+
): DefineTableSchemaResult<TTable, undefined, NoTableSchemaOptions>
|
|
94
|
+
function defineTableSchema<
|
|
95
|
+
TTable extends Table,
|
|
96
|
+
TCoerce extends CoerceOptions = undefined,
|
|
97
|
+
TOptions extends DefineTableSchemaOptions<
|
|
98
|
+
TTable,
|
|
99
|
+
TCoerce
|
|
100
|
+
> = DefineTableSchemaOptions<TTable, TCoerce>,
|
|
101
|
+
>(
|
|
102
|
+
table: TTable,
|
|
103
|
+
options: TOptions
|
|
104
|
+
): DefineTableSchemaResult<TTable, TCoerce, TOptions>
|
|
105
|
+
function defineTableSchema<
|
|
106
|
+
TTable extends Table,
|
|
107
|
+
TCoerce extends CoerceOptions = undefined,
|
|
108
|
+
TOptions extends DefineTableSchemaOptions<
|
|
109
|
+
TTable,
|
|
110
|
+
TCoerce
|
|
111
|
+
> = DefineTableSchemaOptions<TTable, TCoerce>,
|
|
112
|
+
>(
|
|
113
|
+
table: TTable,
|
|
114
|
+
options?: TOptions
|
|
115
|
+
): DefineTableSchemaResult<TTable, TCoerce, TOptions> {
|
|
116
|
+
const schemaFactory = createDrizzleSchemaFactory(options?.factory)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
insert: schemaFactory.createInsertSchema(
|
|
120
|
+
table,
|
|
121
|
+
options?.insert
|
|
122
|
+
) as DefineTableSchemaResult<TTable, TCoerce, TOptions>["insert"],
|
|
123
|
+
select: schemaFactory.createSelectSchema(
|
|
124
|
+
table,
|
|
125
|
+
options?.select
|
|
126
|
+
) as DefineTableSchemaResult<TTable, TCoerce, TOptions>["select"],
|
|
127
|
+
update: schemaFactory.createUpdateSchema(
|
|
128
|
+
table,
|
|
129
|
+
options?.update
|
|
130
|
+
) as DefineTableSchemaResult<TTable, TCoerce, TOptions>["update"],
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type { DefineTableSchemaOptions, DefineTableSchemaResult }
|
|
135
|
+
export { defineTableSchema }
|