@zeno-lib/schema 0.2.2 → 0.4.0

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/src/index.test.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { sql } from "drizzle-orm"
2
2
  import {
3
3
  bigint,
4
+ date,
4
5
  integer,
6
+ pgEnum,
7
+ pgSchema,
5
8
  pgTable,
6
9
  text,
7
10
  timestamp,
@@ -9,15 +12,18 @@ import {
9
12
  } from "drizzle-orm/pg-core"
10
13
  import { describe, expect, it } from "vitest"
11
14
 
12
- import { defineTableSchema } from "./index"
15
+ import { defineTableSchema, writableColumns } from "./index"
16
+ import { zenoTable } from "./test-table"
13
17
 
14
- const posts = pgTable("posts", {
15
- createdAt: timestamp("created_at", { mode: "date" }).notNull().defaultNow(),
16
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
17
- slug: text("slug").notNull(),
18
- summary: text("summary"),
19
- title: text("title").notNull(),
20
- })
18
+ const posts = zenoTable(
19
+ pgTable("posts", {
20
+ createdAt: timestamp("created_at", { mode: "date" }).notNull().defaultNow(),
21
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
22
+ slug: text("slug").notNull(),
23
+ summary: text("summary"),
24
+ title: text("title").notNull(),
25
+ })
26
+ )
21
27
 
22
28
  describe("defineTableSchema", () => {
23
29
  it("returns select, insert, and update Zod schemas for a Drizzle table", () => {
@@ -57,6 +63,13 @@ describe("defineTableSchema", () => {
57
63
  })
58
64
  })
59
65
 
66
+ it("refuses a plain drizzle table, whose column markers would be ignored", () => {
67
+ const raw = pgTable("raw", { label: text("label") })
68
+
69
+ // @ts-expect-error: only a table from `@zeno-lib/db/schema` records rules.
70
+ expect(() => defineTableSchema(raw)).toThrow(TypeError)
71
+ })
72
+
60
73
  it("omits generated columns from insert and update schemas", () => {
61
74
  const schemas = defineTableSchema(posts)
62
75
 
@@ -125,73 +138,54 @@ describe("defineTableSchema", () => {
125
138
  ).toBe(true)
126
139
  })
127
140
 
128
- it("omits defaulted audit columns from insert and update, not select", () => {
129
- const notes = pgTable("notes", {
130
- body: text("body").notNull(),
131
- createdAt: timestamp("created_at").notNull().defaultNow(),
132
- createdBy: uuid("created_by").notNull().default(sql`auth.uid()`),
133
- id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
134
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
135
- updatedBy: uuid("updated_by").default(sql`auth.uid()`),
136
- })
137
- const schemas = defineTableSchema(notes)
138
- const forged = {
139
- body: "Hello",
141
+ it("keeps a column named like an audit column, which only its marker leaves out", () => {
142
+ const events = zenoTable(
143
+ pgTable("events", {
144
+ createdAt: timestamp("created_at").notNull().defaultNow(),
145
+ createdBy: uuid("created_by").default(sql`auth.uid()`),
146
+ label: text("label").notNull(),
147
+ })
148
+ )
149
+ const imported = {
140
150
  createdAt: new Date("2000-01-01T00:00:00.000Z"),
141
151
  createdBy: "00000000-0000-4000-8000-000000000000",
142
- updatedAt: new Date("2000-01-01T00:00:00.000Z"),
143
- updatedBy: "00000000-0000-4000-8000-000000000000",
152
+ label: "x",
144
153
  }
145
154
 
146
- expect(schemas.insert.parse(forged)).toEqual({ body: "Hello" })
147
- expect(schemas.update.parse(forged)).toEqual({ body: "Hello" })
148
- expect(Object.keys(schemas.select.shape)).toEqual(
149
- expect.arrayContaining([
150
- "createdAt",
151
- "createdBy",
152
- "updatedAt",
153
- "updatedBy",
154
- ])
155
- )
155
+ expect(defineTableSchema(events).insert.parse(imported)).toEqual(imported)
156
156
  })
157
157
 
158
- it("keeps an audit-named column the database does not fill", () => {
159
- const events = pgTable("events", {
160
- createdAt: timestamp("created_at").notNull(),
161
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
162
- })
163
-
164
- expect(defineTableSchema(events).insert.safeParse({}).success).toBe(false)
165
- })
158
+ it("keeps identity ids out of insert and update, and positive in select", () => {
159
+ const items = zenoTable(
160
+ pgTable("items", {
161
+ id: bigint("id", { mode: "number" })
162
+ .primaryKey()
163
+ .generatedByDefaultAsIdentity(),
164
+ label: text("label").notNull(),
165
+ })
166
+ )
167
+ const schemas = defineTableSchema(items)
166
168
 
167
- it("rejects non-positive identity ids in every variant", () => {
168
- const items = pgTable("items", {
169
- id: bigint("id", { mode: "number" })
170
- .primaryKey()
171
- .generatedByDefaultAsIdentity(),
172
- label: text("label").notNull(),
169
+ expect(schemas.insert.parse({ id: 5000, label: "x" })).toEqual({
170
+ label: "x",
171
+ })
172
+ expect(schemas.update.parse({ id: 5000, label: "x" })).toEqual({
173
+ label: "x",
173
174
  })
174
- const schemas = defineTableSchema(items)
175
175
 
176
176
  for (const id of [0, -1]) {
177
- expect(schemas.insert.safeParse({ id, label: "x" }).success).toBe(false)
178
- expect(schemas.update.safeParse({ id }).success).toBe(false)
179
177
  expect(schemas.select.safeParse({ id, label: "x" }).success).toBe(false)
180
178
  }
181
-
182
- expect(schemas.insert.parse({ id: 1, label: "x" })).toEqual({
183
- id: 1,
184
- label: "x",
185
- })
186
- expect(schemas.insert.parse({ label: "x" })).toEqual({ label: "x" })
187
179
  })
188
180
 
189
181
  it("leaves identities configured to go below one unconstrained", () => {
190
- const ledger = pgTable("ledger", {
191
- id: integer("id")
192
- .primaryKey()
193
- .generatedByDefaultAsIdentity({ increment: -1, startWith: -1 }),
194
- })
182
+ const ledger = zenoTable(
183
+ pgTable("ledger", {
184
+ id: integer("id")
185
+ .primaryKey()
186
+ .generatedByDefaultAsIdentity({ increment: -1, startWith: -1 }),
187
+ })
188
+ )
195
189
 
196
190
  expect(defineTableSchema(ledger).select.parse({ id: -5 })).toEqual({
197
191
  id: -5,
@@ -199,23 +193,27 @@ describe("defineTableSchema", () => {
199
193
  })
200
194
 
201
195
  it("runs a function refinement on top of the positive identity check", () => {
202
- const items = pgTable("items", {
203
- id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
204
- })
196
+ const items = zenoTable(
197
+ pgTable("items", {
198
+ id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
199
+ })
200
+ )
205
201
  const schemas = defineTableSchema(items, {
206
- insert: { id: (schema) => schema.max(10) },
202
+ select: { id: (schema) => schema.max(10) },
207
203
  })
208
204
 
209
- expect(schemas.insert.safeParse({ id: 0 }).success).toBe(false)
210
- expect(schemas.insert.safeParse({ id: 11 }).success).toBe(false)
211
- expect(schemas.insert.safeParse({ id: 5 }).success).toBe(true)
205
+ expect(schemas.select.safeParse({ id: 0 }).success).toBe(false)
206
+ expect(schemas.select.safeParse({ id: 11 }).success).toBe(false)
207
+ expect(schemas.select.safeParse({ id: 5 }).success).toBe(true)
212
208
  })
213
209
 
214
210
  it("keeps a function-refined nullable insert column optional", () => {
215
- const drafts = pgTable("drafts", {
216
- note: text("note"),
217
- title: text("title").notNull(),
218
- })
211
+ const drafts = zenoTable(
212
+ pgTable("drafts", {
213
+ note: text("note"),
214
+ title: text("title").notNull(),
215
+ })
216
+ )
219
217
  const schemas = defineTableSchema(drafts, {
220
218
  insert: { note: (schema) => schema.min(1) },
221
219
  })
@@ -230,3 +228,78 @@ describe("defineTableSchema", () => {
230
228
  )
231
229
  })
232
230
  })
231
+
232
+ describe("string-mode dates and timestamps", () => {
233
+ const events = zenoTable(
234
+ pgTable("events", {
235
+ at: timestamp("at", { mode: "string", withTimezone: true }),
236
+ day: date("day", { mode: "string" }),
237
+ days: date("days", { mode: "string" }).array(),
238
+ localAt: timestamp("local_at", { mode: "string" }),
239
+ })
240
+ )
241
+ const { insert, update } = defineTableSchema(events)
242
+ const accepts = (value: Record<string, unknown>) =>
243
+ insert.safeParse(value).success && update.safeParse(value).success
244
+
245
+ it("takes a calendar date", () => {
246
+ expect(accepts({ day: "2026-09-17" })).toBe(true)
247
+ expect(accepts({ day: null })).toBe(true)
248
+ expect(accepts({ day: "2026-02-30" })).toBe(false)
249
+ expect(accepts({ day: "17.09.2026" })).toBe(false)
250
+ expect(accepts({ day: "infinity" })).toBe(true)
251
+ expect(accepts({ days: ["2026-09-17", "2026-09-18"] })).toBe(true)
252
+ })
253
+
254
+ it("takes ISO 8601 and the text Postgres returns, with the offset a time zone needs", () => {
255
+ for (const at of [
256
+ "2026-09-17T07:53:56.357127+00:00",
257
+ "2026-09-17 07:53:56.357127+00",
258
+ "2026-09-17T07:53Z",
259
+ ]) {
260
+ expect(accepts({ at }), at).toBe(true)
261
+ }
262
+ expect(accepts({ at: "2026-09-17T07:53:56" })).toBe(false)
263
+ expect(accepts({ at: "yesterday" })).toBe(false)
264
+ expect(accepts({ at: "-infinity" })).toBe(true)
265
+ expect(accepts({ localAt: "2026-09-17 07:53:56.357127" })).toBe(true)
266
+ expect(accepts({ localAt: "yesterday" })).toBe(false)
267
+ })
268
+
269
+ it("refuses a time Postgres would reject or shift", () => {
270
+ expect(accepts({ at: "2026-02-30 07:00+00" })).toBe(false)
271
+ expect(accepts({ at: "2026-09-17T24:00Z" })).toBe(false)
272
+ expect(accepts({ at: "2026-09-17T07:00+16:00" })).toBe(false)
273
+ // Postgres drops an offset on a column without a time zone.
274
+ expect(accepts({ localAt: "2026-09-16T22:00:00.000Z" })).toBe(false)
275
+ })
276
+
277
+ it("lets an object refinement report next to a bad timestamp", () => {
278
+ const result = insert
279
+ .refine(() => false, { message: "cross-field" })
280
+ .safeParse({ at: "yesterday" })
281
+
282
+ expect(result.error?.issues.map((issue) => issue.message)).toContain(
283
+ "cross-field"
284
+ )
285
+ })
286
+ })
287
+
288
+ describe("writableColumns", () => {
289
+ it("lists each Zeno table's writable columns, and skips everything else", () => {
290
+ const status = pgEnum("status", ["open", "closed"])
291
+ const billing = pgSchema("billing")
292
+ const invoices = zenoTable(
293
+ billing.table("invoices", {
294
+ id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
295
+ total: integer("total").notNull(),
296
+ })
297
+ )
298
+ const raw = pgTable("raw", { label: text("label") })
299
+
300
+ expect(writableColumns({ invoices, posts, raw, status })).toEqual({
301
+ "billing.invoices": ["total"],
302
+ posts: ["createdAt", "slug", "summary", "title"],
303
+ })
304
+ })
305
+ })