@zeno-lib/schema 0.1.0 → 0.2.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/dist/index.d.mts +32 -0
- package/dist/index.mjs +52 -0
- package/package.json +11 -4
- package/src/index.test-d.ts +29 -1
- package/src/index.test.ts +95 -1
- package/src/index.ts +151 -25
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { InferInsertModel, InferSelectModel, Table } from "drizzle-orm";
|
|
2
|
+
import { BuildRefine, BuildSchema, CoerceOptions, CreateSchemaFactoryOptions, NoUnknownKeys, createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
declare const noTableSchemaOptions: unique symbol;
|
|
6
|
+
type NoTableSchemaOptions = {
|
|
7
|
+
[noTableSchemaOptions]?: never;
|
|
8
|
+
};
|
|
9
|
+
type TableColumns<TTable extends Table> = TTable["_"]["columns"];
|
|
10
|
+
type AuditColumnKey = "createdAt" | "createdBy" | "updatedAt" | "updatedBy";
|
|
11
|
+
type AuditKeys<TTable extends Table> = { [K in keyof TableColumns<TTable>]: K extends AuditColumnKey ? TableColumns<TTable>[K]["_"]["hasDefault"] extends true ? K : never : never; }[keyof TableColumns<TTable>];
|
|
12
|
+
type InsertColumns<TTable extends Table> = Pick<TableColumns<TTable>, Exclude<keyof InferInsertModel<TTable>, AuditKeys<TTable>>>;
|
|
13
|
+
type SelectRefine<TTable extends Table, TCoerce extends CoerceOptions> = BuildRefine<TableColumns<TTable>, TCoerce>;
|
|
14
|
+
type InsertRefine<TTable extends Table, TCoerce extends CoerceOptions> = BuildRefine<InsertColumns<TTable>, TCoerce>;
|
|
15
|
+
type UpdateRefine<TTable extends Table, TCoerce extends CoerceOptions> = BuildRefine<InsertColumns<TTable>, TCoerce>;
|
|
16
|
+
type DefineTableSchemaOptions<TTable extends Table, TCoerce extends CoerceOptions = undefined> = {
|
|
17
|
+
factory?: CreateSchemaFactoryOptions<TCoerce>;
|
|
18
|
+
insert?: NoUnknownKeys<InsertRefine<TTable, TCoerce>, InferInsertModel<TTable>>;
|
|
19
|
+
select?: NoUnknownKeys<SelectRefine<TTable, TCoerce>, InferSelectModel<TTable>>;
|
|
20
|
+
update?: UpdateRefine<TTable, TCoerce>;
|
|
21
|
+
};
|
|
22
|
+
type RefineFromOptions<TOptions, TKey extends string> = TKey extends keyof TOptions ? NonNullable<TOptions[TKey]> extends Record<string, unknown> ? NonNullable<TOptions[TKey]> : undefined : undefined;
|
|
23
|
+
type OmitKeys<TSchema, TKeys extends PropertyKey> = TSchema extends z.ZodObject<infer TShape, infer TConfig> ? z.ZodObject<{ [K in keyof TShape as K extends TKeys ? never : K]: TShape[K]; }, TConfig> : never;
|
|
24
|
+
type DefineTableSchemaResult<TTable extends Table, TCoerce extends CoerceOptions, TOptions> = {
|
|
25
|
+
insert: OmitKeys<BuildSchema<"insert", TableColumns<TTable>, RefineFromOptions<TOptions, "insert">, TCoerce>, AuditKeys<TTable>>;
|
|
26
|
+
select: BuildSchema<"select", TableColumns<TTable>, RefineFromOptions<TOptions, "select">, TCoerce>;
|
|
27
|
+
update: OmitKeys<BuildSchema<"update", TableColumns<TTable>, RefineFromOptions<TOptions, "update">, TCoerce>, AuditKeys<TTable>>;
|
|
28
|
+
};
|
|
29
|
+
declare function defineTableSchema<TTable extends Table>(table: TTable): DefineTableSchemaResult<TTable, undefined, NoTableSchemaOptions>;
|
|
30
|
+
declare function defineTableSchema<TTable extends Table, TCoerce extends CoerceOptions = undefined, TOptions extends DefineTableSchemaOptions<TTable, TCoerce> = DefineTableSchemaOptions<TTable, TCoerce>>(table: TTable, options: TOptions): DefineTableSchemaResult<TTable, TCoerce, TOptions>;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { type DefineTableSchemaOptions, type DefineTableSchemaResult, createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema, defineTableSchema };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getColumns } from "drizzle-orm";
|
|
2
|
+
import { createInsertSchema, createSchemaFactory, createSchemaFactory as createSchemaFactory$1, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
const AUDIT_COLUMN_KEYS = /* @__PURE__ */ new Set([
|
|
5
|
+
"createdAt",
|
|
6
|
+
"createdBy",
|
|
7
|
+
"updatedAt",
|
|
8
|
+
"updatedBy"
|
|
9
|
+
]);
|
|
10
|
+
const auditKeys = (table) => {
|
|
11
|
+
const columns = getColumns(table);
|
|
12
|
+
const keys = [];
|
|
13
|
+
for (const key of Object.keys(columns)) if (AUDIT_COLUMN_KEYS.has(key) && columns[key]?.hasDefault) keys.push(key);
|
|
14
|
+
return keys;
|
|
15
|
+
};
|
|
16
|
+
const isPositiveIdentity = (column) => {
|
|
17
|
+
const identity = column.generatedIdentity;
|
|
18
|
+
if (identity === void 0) return false;
|
|
19
|
+
const { increment = 1, minValue = 1, startWith = minValue } = identity.sequenceOptions ?? {};
|
|
20
|
+
return Number(increment) > 0 && Number(minValue) > 0 && Number(startWith) > 0;
|
|
21
|
+
};
|
|
22
|
+
const toPositive = (schema) => typeof schema.positive === "function" ? schema.positive() : schema;
|
|
23
|
+
const withPositiveIdentities = (table, refinements = {}) => {
|
|
24
|
+
const columns = getColumns(table);
|
|
25
|
+
const merged = { ...refinements };
|
|
26
|
+
for (const key of Object.keys(columns)) {
|
|
27
|
+
const column = columns[key];
|
|
28
|
+
if (column === void 0 || !isPositiveIdentity(column)) continue;
|
|
29
|
+
const refinement = refinements[key];
|
|
30
|
+
if (refinement === void 0) merged[key] = toPositive;
|
|
31
|
+
else if (typeof refinement === "function") merged[key] = (schema) => refinement(toPositive(schema));
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
};
|
|
35
|
+
function defineTableSchema(table, options) {
|
|
36
|
+
const schemaFactory = createSchemaFactory$1(options?.factory);
|
|
37
|
+
const audit = auditKeys(table);
|
|
38
|
+
const omitAudit = (schema) => {
|
|
39
|
+
const mask = {};
|
|
40
|
+
for (const key of audit) if (key in schema.shape) mask[key] = true;
|
|
41
|
+
return schema.omit(mask);
|
|
42
|
+
};
|
|
43
|
+
const factory = schemaFactory;
|
|
44
|
+
const build = (variant, create) => create(table, withPositiveIdentities(table, options?.[variant]));
|
|
45
|
+
return {
|
|
46
|
+
insert: omitAudit(build("insert", factory.createInsertSchema)),
|
|
47
|
+
select: build("select", factory.createSelectSchema),
|
|
48
|
+
update: omitAudit(build("update", factory.createUpdateSchema))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema, defineTableSchema };
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeno-lib/schema",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
-
".":
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"default": "./dist/index.mjs"
|
|
10
|
+
}
|
|
7
11
|
},
|
|
8
12
|
"files": [
|
|
13
|
+
"dist",
|
|
9
14
|
"src"
|
|
10
15
|
],
|
|
11
16
|
"publishConfig": {
|
|
@@ -21,12 +26,14 @@
|
|
|
21
26
|
},
|
|
22
27
|
"devDependencies": {
|
|
23
28
|
"drizzle-orm": "1.0.0-rc.3",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
29
|
+
"tsdown": "^0.22.14",
|
|
30
|
+
"typescript": "7.0.2",
|
|
31
|
+
"vitest": "4.1.11",
|
|
26
32
|
"zod": "^4.4.3",
|
|
27
33
|
"@zeno-lib/typescript": "^1.1.0"
|
|
28
34
|
},
|
|
29
35
|
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
30
37
|
"test": "vitest --run",
|
|
31
38
|
"test:watch": "vitest watch",
|
|
32
39
|
"types:check": "tsc --noEmit"
|
package/src/index.test-d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sql } from "drizzle-orm"
|
|
2
|
+
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
|
|
2
3
|
import { expectTypeOf, test } from "vitest"
|
|
3
4
|
import { z } from "zod"
|
|
4
5
|
|
|
@@ -52,3 +53,30 @@ test("refined schemas preserve refinement-specific output types", () => {
|
|
|
52
53
|
title: "fixed"
|
|
53
54
|
}>()
|
|
54
55
|
})
|
|
56
|
+
|
|
57
|
+
test("defaulted audit columns leave the insert and update types", () => {
|
|
58
|
+
const notes = pgTable("notes", {
|
|
59
|
+
body: text("body").notNull(),
|
|
60
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
61
|
+
createdBy: uuid("created_by").notNull().default(sql`auth.uid()`),
|
|
62
|
+
updatedAt: timestamp("updated_at").notNull(),
|
|
63
|
+
})
|
|
64
|
+
const schemas = defineTableSchema(notes)
|
|
65
|
+
|
|
66
|
+
expectTypeOf<z.infer<typeof schemas.insert>>().toEqualTypeOf<{
|
|
67
|
+
body: string
|
|
68
|
+
updatedAt: Date
|
|
69
|
+
}>()
|
|
70
|
+
expectTypeOf<z.infer<typeof schemas.update>>().toEqualTypeOf<{
|
|
71
|
+
body?: string | undefined
|
|
72
|
+
updatedAt?: Date | undefined
|
|
73
|
+
}>()
|
|
74
|
+
expectTypeOf<keyof z.infer<typeof schemas.select>>().toEqualTypeOf<
|
|
75
|
+
"body" | "createdAt" | "createdBy" | "updatedAt"
|
|
76
|
+
>()
|
|
77
|
+
|
|
78
|
+
defineTableSchema(notes, {
|
|
79
|
+
// @ts-expect-error: an omitted audit column has nothing to refine.
|
|
80
|
+
insert: { createdBy: (schema) => schema },
|
|
81
|
+
})
|
|
82
|
+
})
|
package/src/index.test.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sql } from "drizzle-orm"
|
|
2
|
+
import {
|
|
3
|
+
bigint,
|
|
4
|
+
integer,
|
|
5
|
+
pgTable,
|
|
6
|
+
text,
|
|
7
|
+
timestamp,
|
|
8
|
+
uuid,
|
|
9
|
+
} from "drizzle-orm/pg-core"
|
|
2
10
|
import { describe, expect, it } from "vitest"
|
|
3
11
|
|
|
4
12
|
import { defineTableSchema } from "./index"
|
|
@@ -116,4 +124,90 @@ describe("defineTableSchema", () => {
|
|
|
116
124
|
}).success
|
|
117
125
|
).toBe(true)
|
|
118
126
|
})
|
|
127
|
+
|
|
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",
|
|
140
|
+
createdAt: new Date("2000-01-01T00:00:00.000Z"),
|
|
141
|
+
createdBy: "00000000-0000-4000-8000-000000000000",
|
|
142
|
+
updatedAt: new Date("2000-01-01T00:00:00.000Z"),
|
|
143
|
+
updatedBy: "00000000-0000-4000-8000-000000000000",
|
|
144
|
+
}
|
|
145
|
+
|
|
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
|
+
)
|
|
156
|
+
})
|
|
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
|
+
})
|
|
166
|
+
|
|
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(),
|
|
173
|
+
})
|
|
174
|
+
const schemas = defineTableSchema(items)
|
|
175
|
+
|
|
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
|
+
expect(schemas.select.safeParse({ id, label: "x" }).success).toBe(false)
|
|
180
|
+
}
|
|
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
|
+
})
|
|
188
|
+
|
|
189
|
+
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
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(defineTableSchema(ledger).select.parse({ id: -5 })).toEqual({
|
|
197
|
+
id: -5,
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("runs a function refinement on top of the positive identity check", () => {
|
|
202
|
+
const items = pgTable("items", {
|
|
203
|
+
id: integer("id").primaryKey().generatedByDefaultAsIdentity(),
|
|
204
|
+
})
|
|
205
|
+
const schemas = defineTableSchema(items, {
|
|
206
|
+
insert: { id: (schema) => schema.max(10) },
|
|
207
|
+
})
|
|
208
|
+
|
|
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)
|
|
212
|
+
})
|
|
119
213
|
})
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/* biome-ignore-all lint/performance/noBarrelFile: package root intentionally re-exports the public schema surface. */
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type Column,
|
|
4
|
+
getColumns,
|
|
5
|
+
type InferInsertModel,
|
|
6
|
+
type InferSelectModel,
|
|
7
|
+
type Table,
|
|
8
|
+
} from "drizzle-orm"
|
|
3
9
|
import {
|
|
4
10
|
type BuildRefine,
|
|
5
11
|
type BuildSchema,
|
|
@@ -8,6 +14,7 @@ import {
|
|
|
8
14
|
createSchemaFactory as createDrizzleSchemaFactory,
|
|
9
15
|
type NoUnknownKeys,
|
|
10
16
|
} from "drizzle-orm/zod"
|
|
17
|
+
import type { z } from "zod"
|
|
11
18
|
|
|
12
19
|
export {
|
|
13
20
|
createInsertSchema,
|
|
@@ -20,9 +27,30 @@ declare const noTableSchemaOptions: unique symbol
|
|
|
20
27
|
type NoTableSchemaOptions = { [noTableSchemaOptions]?: never }
|
|
21
28
|
|
|
22
29
|
type TableColumns<TTable extends Table> = TTable["_"]["columns"]
|
|
30
|
+
|
|
31
|
+
// The keys `timestamps()` and `authorship()` from `@zeno-lib/db/schema` emit.
|
|
32
|
+
// This package cannot import them, so it matches the names instead, and only
|
|
33
|
+
// when the column has a default, since a column the database never fills is
|
|
34
|
+
// one the caller has to write.
|
|
35
|
+
const AUDIT_COLUMN_KEYS = new Set<string>([
|
|
36
|
+
"createdAt",
|
|
37
|
+
"createdBy",
|
|
38
|
+
"updatedAt",
|
|
39
|
+
"updatedBy",
|
|
40
|
+
])
|
|
41
|
+
type AuditColumnKey = "createdAt" | "createdBy" | "updatedAt" | "updatedBy"
|
|
42
|
+
|
|
43
|
+
type AuditKeys<TTable extends Table> = {
|
|
44
|
+
[K in keyof TableColumns<TTable>]: K extends AuditColumnKey
|
|
45
|
+
? TableColumns<TTable>[K]["_"]["hasDefault"] extends true
|
|
46
|
+
? K
|
|
47
|
+
: never
|
|
48
|
+
: never
|
|
49
|
+
}[keyof TableColumns<TTable>]
|
|
50
|
+
|
|
23
51
|
type InsertColumns<TTable extends Table> = Pick<
|
|
24
52
|
TableColumns<TTable>,
|
|
25
|
-
keyof InferInsertModel<TTable
|
|
53
|
+
Exclude<keyof InferInsertModel<TTable>, AuditKeys<TTable>>
|
|
26
54
|
>
|
|
27
55
|
|
|
28
56
|
type SelectRefine<
|
|
@@ -63,16 +91,27 @@ type RefineFromOptions<
|
|
|
63
91
|
: undefined
|
|
64
92
|
: undefined
|
|
65
93
|
|
|
94
|
+
type OmitKeys<TSchema, TKeys extends PropertyKey> =
|
|
95
|
+
TSchema extends z.ZodObject<infer TShape, infer TConfig>
|
|
96
|
+
? z.ZodObject<
|
|
97
|
+
{ [K in keyof TShape as K extends TKeys ? never : K]: TShape[K] },
|
|
98
|
+
TConfig
|
|
99
|
+
>
|
|
100
|
+
: never
|
|
101
|
+
|
|
66
102
|
type DefineTableSchemaResult<
|
|
67
103
|
TTable extends Table,
|
|
68
104
|
TCoerce extends CoerceOptions,
|
|
69
105
|
TOptions,
|
|
70
106
|
> = {
|
|
71
|
-
insert:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
insert: OmitKeys<
|
|
108
|
+
BuildSchema<
|
|
109
|
+
"insert",
|
|
110
|
+
TableColumns<TTable>,
|
|
111
|
+
RefineFromOptions<TOptions, "insert">,
|
|
112
|
+
TCoerce
|
|
113
|
+
>,
|
|
114
|
+
AuditKeys<TTable>
|
|
76
115
|
>
|
|
77
116
|
select: BuildSchema<
|
|
78
117
|
"select",
|
|
@@ -80,14 +119,86 @@ type DefineTableSchemaResult<
|
|
|
80
119
|
RefineFromOptions<TOptions, "select">,
|
|
81
120
|
TCoerce
|
|
82
121
|
>
|
|
83
|
-
update:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
122
|
+
update: OmitKeys<
|
|
123
|
+
BuildSchema<
|
|
124
|
+
"update",
|
|
125
|
+
TableColumns<TTable>,
|
|
126
|
+
RefineFromOptions<TOptions, "update">,
|
|
127
|
+
TCoerce
|
|
128
|
+
>,
|
|
129
|
+
AuditKeys<TTable>
|
|
88
130
|
>
|
|
89
131
|
}
|
|
90
132
|
|
|
133
|
+
type Refinements = Record<string, unknown>
|
|
134
|
+
|
|
135
|
+
const auditKeys = (table: Table) => {
|
|
136
|
+
const columns: Record<string, Column> = getColumns(table)
|
|
137
|
+
const keys: string[] = []
|
|
138
|
+
|
|
139
|
+
for (const key of Object.keys(columns)) {
|
|
140
|
+
if (AUDIT_COLUMN_KEYS.has(key) && columns[key]?.hasDefault) {
|
|
141
|
+
keys.push(key)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return keys
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Postgres starts an ascending identity at its minimum value, which defaults
|
|
149
|
+
// to 1, so only a sequence configured below that can hand out 0 or less.
|
|
150
|
+
const isPositiveIdentity = (column: Column) => {
|
|
151
|
+
const identity = column.generatedIdentity
|
|
152
|
+
|
|
153
|
+
if (identity === undefined) {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const {
|
|
158
|
+
increment = 1,
|
|
159
|
+
minValue = 1,
|
|
160
|
+
startWith = minValue,
|
|
161
|
+
} = identity.sequenceOptions ?? {}
|
|
162
|
+
|
|
163
|
+
return Number(increment) > 0 && Number(minValue) > 0 && Number(startWith) > 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type PositiveSchema = { positive: () => unknown }
|
|
167
|
+
|
|
168
|
+
const toPositive = (schema: unknown) =>
|
|
169
|
+
typeof (schema as Partial<PositiveSchema>).positive === "function"
|
|
170
|
+
? (schema as PositiveSchema).positive()
|
|
171
|
+
: schema
|
|
172
|
+
|
|
173
|
+
// A caller's function refinement runs on the positive schema, so it can narrow
|
|
174
|
+
// further. A caller's schema replaces the column schema outright, positive
|
|
175
|
+
// check included, the same as it replaces every other generated rule.
|
|
176
|
+
const withPositiveIdentities = (
|
|
177
|
+
table: Table,
|
|
178
|
+
refinements: Refinements = {}
|
|
179
|
+
): Refinements => {
|
|
180
|
+
const columns: Record<string, Column> = getColumns(table)
|
|
181
|
+
const merged: Refinements = { ...refinements }
|
|
182
|
+
|
|
183
|
+
for (const key of Object.keys(columns)) {
|
|
184
|
+
const column = columns[key]
|
|
185
|
+
|
|
186
|
+
if (column === undefined || !isPositiveIdentity(column)) {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const refinement = refinements[key]
|
|
191
|
+
|
|
192
|
+
if (refinement === undefined) {
|
|
193
|
+
merged[key] = toPositive
|
|
194
|
+
} else if (typeof refinement === "function") {
|
|
195
|
+
merged[key] = (schema: unknown) => refinement(toPositive(schema))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return merged
|
|
200
|
+
}
|
|
201
|
+
|
|
91
202
|
function defineTableSchema<TTable extends Table>(
|
|
92
203
|
table: TTable
|
|
93
204
|
): DefineTableSchemaResult<TTable, undefined, NoTableSchemaOptions>
|
|
@@ -114,21 +225,36 @@ function defineTableSchema<
|
|
|
114
225
|
options?: TOptions
|
|
115
226
|
): DefineTableSchemaResult<TTable, TCoerce, TOptions> {
|
|
116
227
|
const schemaFactory = createDrizzleSchemaFactory(options?.factory)
|
|
228
|
+
const audit = auditKeys(table)
|
|
229
|
+
// Zod rejects an omit key the shape lacks, which a generated audit column
|
|
230
|
+
// would be.
|
|
231
|
+
const omitAudit = (schema: z.ZodObject) => {
|
|
232
|
+
const mask: Record<string, true> = {}
|
|
117
233
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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"],
|
|
234
|
+
for (const key of audit) {
|
|
235
|
+
if (key in schema.shape) {
|
|
236
|
+
mask[key] = true
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return schema.omit(mask)
|
|
131
241
|
}
|
|
242
|
+
// The drizzle factory's refinement parameters are typed per table, which a
|
|
243
|
+
// generic `TTable` cannot satisfy, so the merged maps go in untyped.
|
|
244
|
+
const factory = schemaFactory as unknown as Record<
|
|
245
|
+
"createInsertSchema" | "createSelectSchema" | "createUpdateSchema",
|
|
246
|
+
(table: Table, refine: Refinements) => z.ZodObject
|
|
247
|
+
>
|
|
248
|
+
const build = (
|
|
249
|
+
variant: "insert" | "select" | "update",
|
|
250
|
+
create: (typeof factory)["createInsertSchema"]
|
|
251
|
+
) => create(table, withPositiveIdentities(table, options?.[variant]))
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
insert: omitAudit(build("insert", factory.createInsertSchema)),
|
|
255
|
+
select: build("select", factory.createSelectSchema),
|
|
256
|
+
update: omitAudit(build("update", factory.createUpdateSchema)),
|
|
257
|
+
} as unknown as DefineTableSchemaResult<TTable, TCoerce, TOptions>
|
|
132
258
|
}
|
|
133
259
|
|
|
134
260
|
export type { DefineTableSchemaOptions, DefineTableSchemaResult }
|