@b9g/zen 0.1.1 → 0.1.3
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/CHANGELOG.md +39 -0
- package/README.md +240 -125
- package/package.json +12 -3
- package/{chunk-W7JTNEM4.js → src/_chunks/chunk-2C6KOX4F.js} +1 -1
- package/{chunk-CHF7L5PC.js → src/_chunks/chunk-2R6FDKLS.js} +1 -1
- package/{chunk-XHXMCOSW.js → src/_chunks/chunk-DKLSJISE.js} +40 -0
- package/{ddl-2A2UFUR3.js → src/_chunks/ddl-32B7E53E.js} +2 -2
- package/src/bun.js +4 -4
- package/src/impl/builtins.d.ts +52 -0
- package/src/impl/database.d.ts +495 -0
- package/src/impl/ddl.d.ts +34 -0
- package/src/impl/errors.d.ts +195 -0
- package/src/impl/query.d.ts +249 -0
- package/src/impl/sql.d.ts +47 -0
- package/src/impl/table.d.ts +961 -0
- package/src/impl/template.d.ts +75 -0
- package/src/mysql.js +3 -3
- package/src/postgres.js +3 -3
- package/src/sqlite.js +3 -3
- package/src/zen.d.ts +6 -7
- package/src/zen.js +5 -2
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table definition with wrapper-based field extensions.
|
|
3
|
+
*
|
|
4
|
+
* Uses wrapper functions instead of .pipe() to avoid Zod internals.
|
|
5
|
+
* Metadata is extracted once at table() call time.
|
|
6
|
+
*/
|
|
7
|
+
import { z, ZodType, ZodObject, ZodRawShape } from "zod";
|
|
8
|
+
import { type SQLBuiltin } from "./builtins.js";
|
|
9
|
+
import { type SQLTemplate } from "./template.js";
|
|
10
|
+
/**
|
|
11
|
+
* Validate data using Standard Schema.
|
|
12
|
+
* All Zod schemas (v3.23+) implement the Standard Schema interface.
|
|
13
|
+
*
|
|
14
|
+
* @internal Used by table methods and database operations
|
|
15
|
+
*/
|
|
16
|
+
export declare function validateWithStandardSchema<T = unknown>(schema: ZodObject<any>, data: unknown): T;
|
|
17
|
+
/**
|
|
18
|
+
* Get database metadata from a schema.
|
|
19
|
+
*
|
|
20
|
+
* Metadata is stored under a namespaced key to avoid collisions with user metadata.
|
|
21
|
+
* Unwraps optional/nullable/default/catch wrappers to find the db metadata.
|
|
22
|
+
*
|
|
23
|
+
* @param schema - Zod schema to read metadata from
|
|
24
|
+
* @returns Database metadata object (empty object if none exists)
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const emailMeta = getDBMeta(emailSchema);
|
|
28
|
+
* if (emailMeta.unique) {
|
|
29
|
+
* console.log("This field has a unique constraint");
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
export declare function getDBMeta(schema: ZodType): Record<string, any>;
|
|
33
|
+
/**
|
|
34
|
+
* Set database metadata on a schema, preserving both user metadata and existing DB metadata.
|
|
35
|
+
*
|
|
36
|
+
* **Declarative only**: Metadata is read once at `table()` construction time.
|
|
37
|
+
* Mutating metadata after a table is defined will NOT affect behavior.
|
|
38
|
+
*
|
|
39
|
+
* **Precedence**: Later calls to setDBMeta() override earlier ones (last write wins).
|
|
40
|
+
* User metadata in the root object is preserved separately from DB metadata.
|
|
41
|
+
*
|
|
42
|
+
* @param schema - Zod schema to attach metadata to
|
|
43
|
+
* @param dbMeta - Database metadata to set (merges with existing DB metadata)
|
|
44
|
+
* @returns New schema with metadata attached
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Define custom field wrapper
|
|
48
|
+
* function hashed<T extends z.ZodString>(schema: T) {
|
|
49
|
+
* return setDBMeta(
|
|
50
|
+
* schema.transform(password => bcrypt.hashSync(password, 10)),
|
|
51
|
+
* { hashed: true }
|
|
52
|
+
* );
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* // User metadata is preserved separately
|
|
56
|
+
* const email = z.string()
|
|
57
|
+
* .email()
|
|
58
|
+
* .meta({ label: "Email Address" }); // User metadata
|
|
59
|
+
* const uniqueEmail = setDBMeta(email, { unique: true }); // DB metadata
|
|
60
|
+
*
|
|
61
|
+
* // Both are accessible:
|
|
62
|
+
* uniqueEmail.meta(); // { label: "Email Address", db: { unique: true } }
|
|
63
|
+
*/
|
|
64
|
+
export declare function setDBMeta<T extends ZodType>(schema: T, dbMeta: Record<string, any>): T;
|
|
65
|
+
export interface FieldDBMeta {
|
|
66
|
+
primaryKey?: boolean;
|
|
67
|
+
unique?: boolean;
|
|
68
|
+
indexed?: boolean;
|
|
69
|
+
softDelete?: boolean;
|
|
70
|
+
reference?: {
|
|
71
|
+
table: Table<any>;
|
|
72
|
+
field?: string;
|
|
73
|
+
as: string;
|
|
74
|
+
onDelete?: "cascade" | "set null" | "restrict";
|
|
75
|
+
};
|
|
76
|
+
encode?: (value: any) => any;
|
|
77
|
+
decode?: (value: any) => any;
|
|
78
|
+
/** Explicit column type override for DDL generation */
|
|
79
|
+
columnType?: string;
|
|
80
|
+
/** Auto-increment flag */
|
|
81
|
+
autoIncrement?: boolean;
|
|
82
|
+
/** Value to apply on INSERT */
|
|
83
|
+
inserted?: {
|
|
84
|
+
type: "sql" | "symbol" | "function";
|
|
85
|
+
/** SQL template (for type: "sql") */
|
|
86
|
+
template?: SQLTemplate;
|
|
87
|
+
symbol?: SQLBuiltin;
|
|
88
|
+
fn?: () => unknown;
|
|
89
|
+
};
|
|
90
|
+
/** Value to apply on UPDATE only */
|
|
91
|
+
updated?: {
|
|
92
|
+
type: "sql" | "symbol" | "function";
|
|
93
|
+
/** SQL template (for type: "sql") */
|
|
94
|
+
template?: SQLTemplate;
|
|
95
|
+
symbol?: SQLBuiltin;
|
|
96
|
+
fn?: () => unknown;
|
|
97
|
+
};
|
|
98
|
+
/** Value to apply on both INSERT and UPDATE */
|
|
99
|
+
upserted?: {
|
|
100
|
+
type: "sql" | "symbol" | "function";
|
|
101
|
+
/** SQL template (for type: "sql") */
|
|
102
|
+
template?: SQLTemplate;
|
|
103
|
+
symbol?: SQLBuiltin;
|
|
104
|
+
fn?: () => unknown;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extend Zod with .db namespace for database-specific methods.
|
|
109
|
+
*
|
|
110
|
+
* Call this once at application startup to add the .db namespace to all Zod types:
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* import {z} from "zod";
|
|
114
|
+
* import {extendZod, table} from "@b9g/zen";
|
|
115
|
+
*
|
|
116
|
+
* extendZod(z);
|
|
117
|
+
*
|
|
118
|
+
* const Users = table("users", {
|
|
119
|
+
* id: z.string().uuid().db.primary(),
|
|
120
|
+
* email: z.string().email().db.unique(),
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* @param zodModule - The Zod module to extend (typically `z` from `import {z} from "zod"`)
|
|
124
|
+
*/
|
|
125
|
+
export declare function extendZod(zodModule: typeof z): void;
|
|
126
|
+
export type FieldType = "text" | "textarea" | "email" | "url" | "tel" | "password" | "number" | "integer" | "checkbox" | "select" | "date" | "datetime" | "time" | "json" | "hidden";
|
|
127
|
+
export interface FieldMeta {
|
|
128
|
+
name: string;
|
|
129
|
+
type: FieldType;
|
|
130
|
+
required: boolean;
|
|
131
|
+
primaryKey?: boolean;
|
|
132
|
+
unique?: boolean;
|
|
133
|
+
indexed?: boolean;
|
|
134
|
+
softDelete?: boolean;
|
|
135
|
+
reference?: {
|
|
136
|
+
table: Table;
|
|
137
|
+
field: string;
|
|
138
|
+
as: string;
|
|
139
|
+
onDelete?: "cascade" | "set null" | "restrict";
|
|
140
|
+
};
|
|
141
|
+
encode?: (value: any) => any;
|
|
142
|
+
decode?: (value: any) => any;
|
|
143
|
+
columnType?: string;
|
|
144
|
+
autoIncrement?: boolean;
|
|
145
|
+
inserted?: {
|
|
146
|
+
type: "sql" | "symbol" | "function";
|
|
147
|
+
template?: SQLTemplate;
|
|
148
|
+
symbol?: SQLBuiltin;
|
|
149
|
+
fn?: () => unknown;
|
|
150
|
+
};
|
|
151
|
+
updated?: {
|
|
152
|
+
type: "sql" | "symbol" | "function";
|
|
153
|
+
template?: SQLTemplate;
|
|
154
|
+
symbol?: SQLBuiltin;
|
|
155
|
+
fn?: () => unknown;
|
|
156
|
+
};
|
|
157
|
+
upserted?: {
|
|
158
|
+
type: "sql" | "symbol" | "function";
|
|
159
|
+
template?: SQLTemplate;
|
|
160
|
+
symbol?: SQLBuiltin;
|
|
161
|
+
fn?: () => unknown;
|
|
162
|
+
};
|
|
163
|
+
default?: unknown;
|
|
164
|
+
maxLength?: number;
|
|
165
|
+
minLength?: number;
|
|
166
|
+
min?: number;
|
|
167
|
+
max?: number;
|
|
168
|
+
options?: readonly string[];
|
|
169
|
+
/** Additional user-defined metadata from Zod's .meta() (label, helpText, widget, etc.) */
|
|
170
|
+
[key: string]: unknown;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Compound foreign key reference defined at the table level.
|
|
174
|
+
*/
|
|
175
|
+
export interface CompoundReference {
|
|
176
|
+
/** Local field names that form the foreign key */
|
|
177
|
+
fields: string[];
|
|
178
|
+
/** Referenced table */
|
|
179
|
+
table: Table<any>;
|
|
180
|
+
/** Referenced field names (must match fields length, defaults to referenced table's fields) */
|
|
181
|
+
referencedFields?: string[];
|
|
182
|
+
/** Property name for the resolved reference */
|
|
183
|
+
as: string;
|
|
184
|
+
/** Delete behavior */
|
|
185
|
+
onDelete?: "cascade" | "set null" | "restrict";
|
|
186
|
+
}
|
|
187
|
+
export interface TableOptions<TRow = any, TDerive extends Record<string, (entity: TRow) => unknown> = Record<string, (entity: TRow) => unknown>> {
|
|
188
|
+
/** Compound indexes */
|
|
189
|
+
indexes?: string[][];
|
|
190
|
+
/** Compound unique constraints */
|
|
191
|
+
unique?: string[][];
|
|
192
|
+
/** Compound foreign key references */
|
|
193
|
+
references?: CompoundReference[];
|
|
194
|
+
/**
|
|
195
|
+
* Derived properties - client-side transformations of already-fetched data.
|
|
196
|
+
*
|
|
197
|
+
* Derived properties:
|
|
198
|
+
* - Are NOT stored in the database
|
|
199
|
+
* - ARE part of TypeScript type inference (via Row<T>)
|
|
200
|
+
* - Are lazy getters (computed on access)
|
|
201
|
+
* - Are non-enumerable (don't appear in Object.keys() or JSON.stringify())
|
|
202
|
+
* - Must be pure functions (no I/O, no side effects)
|
|
203
|
+
* - Only transform data already in the entity
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const Posts = table("posts", schema, {
|
|
207
|
+
* derive: {
|
|
208
|
+
* titleUpper: (post) => post.title.toUpperCase(),
|
|
209
|
+
* }
|
|
210
|
+
* });
|
|
211
|
+
*
|
|
212
|
+
* type Post = Row<typeof Posts>; // includes titleUpper: string
|
|
213
|
+
* post.titleUpper // ✅ Typed as string
|
|
214
|
+
* JSON.stringify(post) // ✅ Doesn't include titleUpper (non-enumerable)
|
|
215
|
+
*/
|
|
216
|
+
derive?: TDerive;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Infer the derived property types from a TableOptions derive object.
|
|
220
|
+
*/
|
|
221
|
+
export type InferDerived<TDerive> = TDerive extends Record<string, (entity: any) => unknown> ? {
|
|
222
|
+
[K in keyof TDerive]: TDerive[K] extends (entity: any) => infer R ? R : never;
|
|
223
|
+
} : {};
|
|
224
|
+
declare const TABLE_MARKER: unique symbol;
|
|
225
|
+
declare const TABLE_META: unique symbol;
|
|
226
|
+
/**
|
|
227
|
+
* Check if a value is a Table object.
|
|
228
|
+
*/
|
|
229
|
+
export declare function isTable(value: unknown): value is Table<any>;
|
|
230
|
+
/** Internal table metadata type */
|
|
231
|
+
export interface TableMeta {
|
|
232
|
+
primary: string | null;
|
|
233
|
+
unique: string[];
|
|
234
|
+
indexed: string[];
|
|
235
|
+
softDeleteField: string | null;
|
|
236
|
+
references: ReferenceInfo[];
|
|
237
|
+
fields: Record<string, FieldDBMeta>;
|
|
238
|
+
derive?: Record<string, (entity: any) => any>;
|
|
239
|
+
isPartial?: boolean;
|
|
240
|
+
isDerived?: boolean;
|
|
241
|
+
derivedExprs?: DerivedExpr[];
|
|
242
|
+
derivedFields?: string[];
|
|
243
|
+
/** True if this table represents a view (read-only, no mutations) */
|
|
244
|
+
isView?: boolean;
|
|
245
|
+
/** The base table name this view is derived from */
|
|
246
|
+
viewOf?: string;
|
|
247
|
+
/** The active view for soft delete (auto-created when .active is accessed) */
|
|
248
|
+
activeView?: View<any>;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get internal metadata from a Table.
|
|
252
|
+
* For internal library use only - not part of public API.
|
|
253
|
+
*/
|
|
254
|
+
export declare function getTableMeta(table: Queryable<any>): TableMeta;
|
|
255
|
+
export interface ReferenceInfo {
|
|
256
|
+
fieldName: string;
|
|
257
|
+
table: Table<any>;
|
|
258
|
+
referencedField: string;
|
|
259
|
+
as: string;
|
|
260
|
+
reverseAs?: string;
|
|
261
|
+
onDelete?: "cascade" | "set null" | "restrict";
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Metadata for SQL-derived expressions created via Table.derive().
|
|
265
|
+
*/
|
|
266
|
+
export interface DerivedExpr {
|
|
267
|
+
/** The field name for this derived column */
|
|
268
|
+
fieldName: string;
|
|
269
|
+
/** SQL template for the expression */
|
|
270
|
+
template: SQLTemplate;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Set values for updates - schema columns only (excludes derived properties).
|
|
274
|
+
*/
|
|
275
|
+
export type SetValues<T extends Table<any>> = {
|
|
276
|
+
[K in keyof z.infer<T["schema"]>]?: z.infer<T["schema"]>[K];
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Unwrap Zod wrapper types (optional, nullable, default, catch) at the type level.
|
|
280
|
+
* Used to extract reference metadata from wrapped schemas.
|
|
281
|
+
*/
|
|
282
|
+
type UnwrapZod<T> = T extends z.ZodOptional<infer U> ? UnwrapZod<U> : T extends z.ZodNullable<infer U> ? UnwrapZod<U> : T extends z.ZodDefault<infer U> ? UnwrapZod<U> : T extends z.ZodCatch<infer U> ? UnwrapZod<U> : T;
|
|
283
|
+
/**
|
|
284
|
+
* Extract relationship references from a table schema.
|
|
285
|
+
* Maps relationship aliases (the "as" name) to their target tables.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* const Posts = table("posts", {
|
|
289
|
+
* authorId: z.string().db.references(Users, "author"),
|
|
290
|
+
* });
|
|
291
|
+
* type PostRefs = RowRefs<typeof Posts["schema"]["shape"]>;
|
|
292
|
+
* // { author: typeof Users }
|
|
293
|
+
*/
|
|
294
|
+
export type RowRefs<T extends ZodRawShape> = {
|
|
295
|
+
[K in keyof T as UnwrapZod<T[K]> extends {
|
|
296
|
+
readonly __refAs: infer As;
|
|
297
|
+
} ? As extends string ? As : never : never]: UnwrapZod<T[K]> extends {
|
|
298
|
+
readonly __refTable: infer RefTab extends Table<any, any>;
|
|
299
|
+
} ? RefTab : never;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Filter refs to only include those whose backing field is in the picked schema.
|
|
303
|
+
* Used by pick() to correctly narrow relationship types.
|
|
304
|
+
*/
|
|
305
|
+
type FilterRefs<PickedShape extends ZodRawShape, OriginalRefs extends Record<string, Table<any, any>>> = {
|
|
306
|
+
[Alias in keyof OriginalRefs as Alias extends keyof RowRefs<PickedShape> ? Alias : never]: OriginalRefs[Alias];
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* A relationship jump point for navigating to a referenced table's fields.
|
|
310
|
+
*/
|
|
311
|
+
export interface Relation<TargetTable extends Table<any, any>> {
|
|
312
|
+
/** Navigate to the target table's fields */
|
|
313
|
+
fields(): ReturnType<TargetTable["fields"]>;
|
|
314
|
+
/** Direct access to the referenced table */
|
|
315
|
+
readonly table: TargetTable;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The return type of `table.fields()` - combines column fields with relationship navigators.
|
|
319
|
+
*/
|
|
320
|
+
export type TableFields<T extends ZodRawShape, Refs extends Record<string, Table<any, any>>> = {
|
|
321
|
+
[K in keyof T]: FieldMeta;
|
|
322
|
+
} & {
|
|
323
|
+
[K in keyof Refs]: Relation<Refs[K]>;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* A partial view of a table created via pick().
|
|
327
|
+
* Can be used for queries but not for insert().
|
|
328
|
+
* Check via table.meta.isPartial at runtime.
|
|
329
|
+
*/
|
|
330
|
+
export interface PartialTable<T extends ZodRawShape = ZodRawShape, Refs extends Record<string, Table<any, any>> = {}> extends Table<T, Refs> {
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* A table with SQL-derived columns created via derive().
|
|
334
|
+
* Can be used for queries but not for insert/update.
|
|
335
|
+
* Check via table.meta.isDerived at runtime.
|
|
336
|
+
*/
|
|
337
|
+
export interface DerivedTable<T extends ZodRawShape = ZodRawShape, Refs extends Record<string, Table<any, any>> = {}> extends Table<T, Refs> {
|
|
338
|
+
}
|
|
339
|
+
export interface Table<T extends ZodRawShape = ZodRawShape, Refs extends Record<string, Table<any>> = {}, _Derived extends Record<string, unknown> = {}> {
|
|
340
|
+
readonly [TABLE_MARKER]: true;
|
|
341
|
+
/** @internal Symbol-keyed internal metadata */
|
|
342
|
+
readonly [TABLE_META]: TableMeta;
|
|
343
|
+
readonly name: string;
|
|
344
|
+
readonly schema: ZodObject<T>;
|
|
345
|
+
readonly indexes: string[][];
|
|
346
|
+
readonly unique: string[][];
|
|
347
|
+
readonly compoundReferences: CompoundReference[];
|
|
348
|
+
/**
|
|
349
|
+
* @internal Internal table metadata. Use getTableMeta() or table methods instead.
|
|
350
|
+
*/
|
|
351
|
+
readonly meta: TableMeta;
|
|
352
|
+
/** Get field metadata for forms/admin, with relationship navigators */
|
|
353
|
+
fields(): TableFields<T, Refs>;
|
|
354
|
+
/**
|
|
355
|
+
* Get the primary key field name.
|
|
356
|
+
*
|
|
357
|
+
* @returns The primary key field name, or null if no primary key is defined.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* const pk = Users.primaryKey(); // "id"
|
|
361
|
+
*/
|
|
362
|
+
primaryKey(): string | null;
|
|
363
|
+
/**
|
|
364
|
+
* Fully qualified primary key column as SQL fragment.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* db.all(Posts)`GROUP BY ${Posts.primary}`
|
|
368
|
+
* // → GROUP BY "posts"."id"
|
|
369
|
+
*/
|
|
370
|
+
readonly primary: SQLTemplate | null;
|
|
371
|
+
/** Get all foreign key references */
|
|
372
|
+
references(): ReferenceInfo[];
|
|
373
|
+
/**
|
|
374
|
+
* Generate SQL fragment to check if a row is soft-deleted.
|
|
375
|
+
*
|
|
376
|
+
* Returns `"table"."deleted_at" IS NOT NULL` where deleted_at is the field
|
|
377
|
+
* marked with softDelete().
|
|
378
|
+
*
|
|
379
|
+
* @throws Error if table doesn't have a soft delete field
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* // Exclude soft-deleted records
|
|
383
|
+
* db.all(Posts)`WHERE NOT (${Posts.deleted()}) AND published = ${true}`
|
|
384
|
+
*
|
|
385
|
+
* // Show only soft-deleted records
|
|
386
|
+
* db.all(Posts)`WHERE ${Posts.deleted()}`
|
|
387
|
+
*/
|
|
388
|
+
deleted(): SQLTemplate;
|
|
389
|
+
/**
|
|
390
|
+
* Generate safe IN clause with proper parameterization.
|
|
391
|
+
*
|
|
392
|
+
* Prevents SQL injection and handles empty arrays correctly.
|
|
393
|
+
*
|
|
394
|
+
* @throws Error if field doesn't exist in table
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* db.all(Posts)`WHERE ${Posts.in("id", postIds)}`
|
|
398
|
+
* // Generates: "posts"."id" IN (?, ?, ?)
|
|
399
|
+
*
|
|
400
|
+
* // Empty array returns FALSE
|
|
401
|
+
* db.all(Posts)`WHERE ${Posts.in("id", [])}`
|
|
402
|
+
* // Generates: 1 = 0
|
|
403
|
+
*/
|
|
404
|
+
in<K extends keyof z.infer<ZodObject<T>>>(field: K, values: unknown[]): SQLTemplate;
|
|
405
|
+
/**
|
|
406
|
+
* Create a partial view of this table with only the specified fields.
|
|
407
|
+
*
|
|
408
|
+
* Useful for partial selects - the returned table-like object can be
|
|
409
|
+
* passed to all(), get(), etc. Cannot be used with insert().
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* const PostSummary = Posts.pick('id', 'title', 'authorId');
|
|
413
|
+
* db.all(PostSummary, Users.pick('id', 'name'))`...`
|
|
414
|
+
*/
|
|
415
|
+
pick<K extends keyof z.infer<ZodObject<T>>>(...fields: K[]): PartialTable<Pick<T, K & keyof T>, FilterRefs<Pick<T, K & keyof T>, Refs>>;
|
|
416
|
+
/**
|
|
417
|
+
* Create a new table with SQL-computed derived columns.
|
|
418
|
+
*
|
|
419
|
+
* Returns a tagged template function that parses the SQL expression.
|
|
420
|
+
* Derived columns are computed in the SELECT clause by the database.
|
|
421
|
+
* The AS aliases must match the schema field names.
|
|
422
|
+
*
|
|
423
|
+
* The returned table cannot be used for insert/update - SELECT only.
|
|
424
|
+
*
|
|
425
|
+
* @param derivedSchema - Zod schema for the derived fields
|
|
426
|
+
* @returns Tagged template function that returns a DerivedTable
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* const PostsWithStats = Posts
|
|
430
|
+
* .derive('likeCount', z.number())`COUNT(DISTINCT ${Likes.cols.id})`
|
|
431
|
+
* .derive('commentCount', z.number())`COUNT(DISTINCT ${Comments.cols.id})`;
|
|
432
|
+
*
|
|
433
|
+
* db.all(PostsWithStats, Likes, Comments)`
|
|
434
|
+
* LEFT JOIN likes ON ${Likes.cols.postId} = ${Posts.cols.id}
|
|
435
|
+
* LEFT JOIN comments ON ${Comments.cols.postId} = ${Posts.cols.id}
|
|
436
|
+
* GROUP BY ${Posts.primary}
|
|
437
|
+
* `
|
|
438
|
+
*/
|
|
439
|
+
derive<N extends string, V extends z.ZodType>(fieldName: N, fieldType: V): (strings: TemplateStringsArray, ...values: unknown[]) => DerivedTable<T & {
|
|
440
|
+
[K in N]: V;
|
|
441
|
+
}, Refs>;
|
|
442
|
+
/**
|
|
443
|
+
* Access qualified column names as SQL fragments.
|
|
444
|
+
*
|
|
445
|
+
* Each property returns a fragment with the fully qualified, quoted column name.
|
|
446
|
+
* Useful for JOINs, ORDER BY, GROUP BY, or disambiguating columns.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* db.all(Posts, Users)`
|
|
450
|
+
* JOIN users ON ${Users.cols.id} = ${Posts.cols.authorId}
|
|
451
|
+
* WHERE ${Posts.cols.published} = ${true}
|
|
452
|
+
* ORDER BY ${Posts.cols.createdAt} DESC
|
|
453
|
+
* `
|
|
454
|
+
* // → JOIN users ON "users"."id" = "posts"."authorId" WHERE "posts"."published" = ? ORDER BY "posts"."createdAt" DESC
|
|
455
|
+
*/
|
|
456
|
+
readonly cols: {
|
|
457
|
+
[K in keyof z.infer<ZodObject<T>>]: SQLTemplate;
|
|
458
|
+
};
|
|
459
|
+
/**
|
|
460
|
+
* Generate assignment fragment for UPDATE SET clauses.
|
|
461
|
+
*
|
|
462
|
+
* Column names are quoted but not table-qualified (SQL UPDATE syntax).
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* db.exec`
|
|
466
|
+
* UPDATE posts SET ${Posts.set({ title: "New Title" })} WHERE id = ${id}
|
|
467
|
+
* `
|
|
468
|
+
* // → "title" = ?
|
|
469
|
+
*/
|
|
470
|
+
set(values: SetValues<Table<T>>): SQLTemplate;
|
|
471
|
+
/**
|
|
472
|
+
* Generate the ON condition for a JOIN clause (FK equality).
|
|
473
|
+
*
|
|
474
|
+
* Called on the table being joined, with the referencing table as argument.
|
|
475
|
+
* Looks up foreign keys in the referencing table that point to this table.
|
|
476
|
+
* Returns just the equality condition; you write the JOIN clause.
|
|
477
|
+
*
|
|
478
|
+
* @param referencingTable - The table that has a foreign key to this table
|
|
479
|
+
* @param alias - Optional relationship alias (the "as" name) to disambiguate
|
|
480
|
+
* when multiple FKs point to the same table
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* // Single FK - no disambiguation needed
|
|
484
|
+
* db.all([Posts, Users])`
|
|
485
|
+
* JOIN "users" ON ${Users.on(Posts)}
|
|
486
|
+
* WHERE ${Posts.where({published: true})}
|
|
487
|
+
* `
|
|
488
|
+
* // → JOIN "users" ON "users"."id" = "posts"."authorId" WHERE ...
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* // Multiple FKs to same table - use alias to disambiguate
|
|
492
|
+
* const Posts = table("posts", {
|
|
493
|
+
* authorId: z.string().db.references(Users, "author"),
|
|
494
|
+
* editorId: z.string().db.references(Users, "editor"),
|
|
495
|
+
* });
|
|
496
|
+
* db.all([Posts, Users])`
|
|
497
|
+
* JOIN "users" AS "author" ON ${Users.on(Posts, "author")}
|
|
498
|
+
* JOIN "users" AS "editor" ON ${Users.on(Posts, "editor")}
|
|
499
|
+
* WHERE ...
|
|
500
|
+
* `
|
|
501
|
+
* // → JOIN "users" AS "author" ON "users"."id" = "posts"."authorId"
|
|
502
|
+
* // JOIN "users" AS "editor" ON "users"."id" = "posts"."editorId" WHERE ...
|
|
503
|
+
*/
|
|
504
|
+
on(referencingTable: Table<any>, alias?: string): SQLTemplate;
|
|
505
|
+
/**
|
|
506
|
+
* Generate column list and value tuples for INSERT statements.
|
|
507
|
+
*
|
|
508
|
+
* Columns are inferred from the first row's keys. All rows must have
|
|
509
|
+
* the same columns. Each row is validated against the table schema.
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* db.exec`
|
|
513
|
+
* INSERT INTO posts ${Posts.values([
|
|
514
|
+
* {id: "1", title: "First"},
|
|
515
|
+
* {id: "2", title: "Second"},
|
|
516
|
+
* ])}
|
|
517
|
+
* `
|
|
518
|
+
* // → INSERT INTO posts (id, title) VALUES (?, ?), (?, ?)
|
|
519
|
+
*/
|
|
520
|
+
values(rows: Partial<z.infer<ZodObject<T>>>[]): SQLTemplate;
|
|
521
|
+
/**
|
|
522
|
+
* Get a view that excludes soft-deleted rows.
|
|
523
|
+
*
|
|
524
|
+
* Returns a read-only View named `{table}_active` (e.g., `users_active`)
|
|
525
|
+
* that filters out rows where the soft delete field is not null.
|
|
526
|
+
*
|
|
527
|
+
* The view is read-only - use the base table for mutations.
|
|
528
|
+
*
|
|
529
|
+
* @throws Error if table doesn't have a soft delete field
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* // Define table with soft delete
|
|
533
|
+
* const Users = table("users", {
|
|
534
|
+
* id: z.string().db.primary(),
|
|
535
|
+
* name: z.string(),
|
|
536
|
+
* deletedAt: z.date().nullable().db.softDelete(),
|
|
537
|
+
* });
|
|
538
|
+
*
|
|
539
|
+
* // Query only active (non-deleted) users
|
|
540
|
+
* const activeUsers = await db.all(Users.active)``;
|
|
541
|
+
*
|
|
542
|
+
* // JOINs automatically filter deleted rows
|
|
543
|
+
* await db.all([Posts, Users.active])`
|
|
544
|
+
* JOIN "users_active" ON ${Users.active.cols.id} = ${Posts.cols.authorId}
|
|
545
|
+
* `;
|
|
546
|
+
*
|
|
547
|
+
* // Views are read-only
|
|
548
|
+
* await db.insert(Users.active, {...}); // ✗ Throws error
|
|
549
|
+
*/
|
|
550
|
+
readonly active: View<T, Refs>;
|
|
551
|
+
}
|
|
552
|
+
/** Internal view metadata */
|
|
553
|
+
export interface ViewMeta {
|
|
554
|
+
/** The base table this view is derived from */
|
|
555
|
+
baseTable: Table<any>;
|
|
556
|
+
/** SQL template for the WHERE clause */
|
|
557
|
+
whereTemplate: SQLTemplate;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* A database view - a read-only projection of a table with a WHERE clause.
|
|
561
|
+
*
|
|
562
|
+
* Views are virtual tables that filter rows from a base table.
|
|
563
|
+
* They can be queried like tables but do not support mutations.
|
|
564
|
+
*/
|
|
565
|
+
export interface View<T extends ZodRawShape = ZodRawShape, Refs extends Record<string, Table> = Record<string, Table>> {
|
|
566
|
+
/** The view name */
|
|
567
|
+
readonly name: string;
|
|
568
|
+
/** Zod schema for validating rows */
|
|
569
|
+
readonly schema: ZodObject<T>;
|
|
570
|
+
/** Internal metadata (for library use) */
|
|
571
|
+
readonly meta: TableMeta;
|
|
572
|
+
/** Column reference fragments for SQL templates */
|
|
573
|
+
readonly cols: {
|
|
574
|
+
readonly [K in keyof T]: SQLTemplate;
|
|
575
|
+
};
|
|
576
|
+
/** Primary key column fragment */
|
|
577
|
+
readonly primary: SQLTemplate | null;
|
|
578
|
+
/**
|
|
579
|
+
* Get field metadata for all columns.
|
|
580
|
+
* Returns an object mapping column names to their metadata.
|
|
581
|
+
*/
|
|
582
|
+
fields(): Record<string, FieldMeta>;
|
|
583
|
+
/**
|
|
584
|
+
* Get reference metadata for foreign key relationships.
|
|
585
|
+
* Delegates to the base table's references.
|
|
586
|
+
*/
|
|
587
|
+
references(): ReferenceInfo[];
|
|
588
|
+
/**
|
|
589
|
+
* Get the base table this view is derived from.
|
|
590
|
+
*/
|
|
591
|
+
readonly baseTable: Table<T, Refs>;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Check if a value is a View object.
|
|
595
|
+
*/
|
|
596
|
+
export declare function isView(value: unknown): value is View<any>;
|
|
597
|
+
/**
|
|
598
|
+
* Get internal metadata from a View.
|
|
599
|
+
* For internal library use only - not part of public API.
|
|
600
|
+
*/
|
|
601
|
+
export declare function getViewMeta(view: View<any>): ViewMeta;
|
|
602
|
+
/**
|
|
603
|
+
* Define a database view based on a table with a WHERE clause.
|
|
604
|
+
*
|
|
605
|
+
* Views are read-only projections of tables. They can be queried
|
|
606
|
+
* like tables but do not support insert, update, or delete.
|
|
607
|
+
*
|
|
608
|
+
* @param name - The full view name (e.g., "active_users")
|
|
609
|
+
* @param baseTable - The table this view is based on
|
|
610
|
+
* @returns Tagged template function that accepts the WHERE clause
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* const Users = table("users", {
|
|
614
|
+
* id: z.string().db.primary(),
|
|
615
|
+
* name: z.string(),
|
|
616
|
+
* role: z.string(),
|
|
617
|
+
* deletedAt: z.date().nullable().db.softDelete(),
|
|
618
|
+
* });
|
|
619
|
+
*
|
|
620
|
+
* // Define views with explicit names
|
|
621
|
+
* const ActiveUsers = view("active_users", Users)`
|
|
622
|
+
* WHERE ${Users.cols.deletedAt} IS NULL
|
|
623
|
+
* `;
|
|
624
|
+
*
|
|
625
|
+
* const AdminUsers = view("admin_users", Users)`
|
|
626
|
+
* WHERE ${Users.cols.role} = ${"admin"}
|
|
627
|
+
* `;
|
|
628
|
+
*
|
|
629
|
+
* // Query from view
|
|
630
|
+
* const admins = await db.all(AdminUsers)``;
|
|
631
|
+
*
|
|
632
|
+
* // Views are read-only
|
|
633
|
+
* await db.insert(AdminUsers, {...}); // ✗ Throws error
|
|
634
|
+
*/
|
|
635
|
+
export declare function view<T extends ZodRawShape, Refs extends Record<string, Table>>(name: string, baseTable: Table<T, Refs>): (strings: TemplateStringsArray, ...values: unknown[]) => View<T, Refs>;
|
|
636
|
+
/**
|
|
637
|
+
* Define a database table with a Zod schema.
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* const users = table("users", {
|
|
641
|
+
* id: primary(z.string().uuid()),
|
|
642
|
+
* email: unique(z.string().email()),
|
|
643
|
+
* name: z.string().max(100),
|
|
644
|
+
* role: z.enum(["user", "admin"]).default("user"),
|
|
645
|
+
* });
|
|
646
|
+
*/
|
|
647
|
+
export declare function table<T extends Record<string, ZodType>, TDerive extends Record<string, (entity: z.infer<ZodObject<T>>) => unknown> = {}>(name: string, shape: T, options?: TableOptions<z.infer<ZodObject<T>>, TDerive>): Table<T, RowRefs<T>, InferDerived<TDerive>>;
|
|
648
|
+
/**
|
|
649
|
+
* A queryable source - either a Table or a View.
|
|
650
|
+
* Used for read operations that can accept either type.
|
|
651
|
+
*/
|
|
652
|
+
export type Queryable<T extends ZodRawShape = ZodRawShape, Refs extends Record<string, Table> = Record<string, Table>> = Table<T, Refs> | View<T, Refs>;
|
|
653
|
+
/**
|
|
654
|
+
* Extract the Derived type parameter from a Table.
|
|
655
|
+
*/
|
|
656
|
+
type GetDerived<T> = T extends Table<any, any, infer D> ? D : {};
|
|
657
|
+
/**
|
|
658
|
+
* Infer the row type from a table or view (full entity after read).
|
|
659
|
+
* Includes derived properties if defined.
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* const Users = table("users", {...});
|
|
663
|
+
* type User = Row<typeof Users>;
|
|
664
|
+
*
|
|
665
|
+
* // With derived properties:
|
|
666
|
+
* const Posts = table("posts", {...}, {
|
|
667
|
+
* derive: { titleUpper: (p) => p.title.toUpperCase() }
|
|
668
|
+
* });
|
|
669
|
+
* type Post = Row<typeof Posts>; // includes titleUpper: string
|
|
670
|
+
*/
|
|
671
|
+
export type Row<T extends Queryable<any>> = z.infer<T["schema"]> & GetDerived<T>;
|
|
672
|
+
/**
|
|
673
|
+
* Extract the Refs type parameter from a Table or View.
|
|
674
|
+
*/
|
|
675
|
+
type GetRefs<T extends Queryable<any, any>> = T extends Table<any, infer R> ? R : T extends View<any, infer R> ? R : {};
|
|
676
|
+
/**
|
|
677
|
+
* Given a primary table/view and a tuple of joined tables/views, build an object type
|
|
678
|
+
* that maps each matching ref alias to Row<RefTable>.
|
|
679
|
+
*
|
|
680
|
+
* For example, if Posts has `{ author: typeof Users }` refs and Users is in JoinedTables,
|
|
681
|
+
* this produces `{ author: Row<typeof Users> }`.
|
|
682
|
+
*/
|
|
683
|
+
type ResolvedRow<Refs extends Record<string, Table<any, any>>, JoinedTables extends readonly Queryable<any, any>[]> = {
|
|
684
|
+
[Alias in keyof Refs as Refs[Alias] extends JoinedTables[number] ? Alias : never]: Refs[Alias] extends Queryable<any, any> ? Row<Refs[Alias]> | null : never;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* A row type with resolved relationship properties from joined tables.
|
|
688
|
+
*
|
|
689
|
+
* When you query `db.all([Posts, Users])`, this type produces:
|
|
690
|
+
* `Row<Posts> & { author?: Row<Users> }`
|
|
691
|
+
*
|
|
692
|
+
* @example
|
|
693
|
+
* const posts = await db.all([Posts, Users])`JOIN users ON ...`;
|
|
694
|
+
* posts[0].author?.name; // typed as string | undefined
|
|
695
|
+
*/
|
|
696
|
+
export type JoinedRow<PrimaryTable extends Queryable<any, any>, JoinedTables extends readonly Queryable<any, any>[]> = Row<PrimaryTable> & ResolvedRow<GetRefs<PrimaryTable>, JoinedTables>;
|
|
697
|
+
/**
|
|
698
|
+
* Type guard that evaluates to `never` for partial or derived tables.
|
|
699
|
+
* Use this to prevent insert/update operations at compile time.
|
|
700
|
+
*
|
|
701
|
+
* @example
|
|
702
|
+
* function insert<T extends Table<any>>(
|
|
703
|
+
* table: T & FullTableOnly<T>,
|
|
704
|
+
* data: Insert<T>
|
|
705
|
+
* ): Promise<Row<T>>
|
|
706
|
+
*/
|
|
707
|
+
export type FullTableOnly<T> = T extends {
|
|
708
|
+
meta: {
|
|
709
|
+
isPartial: true;
|
|
710
|
+
};
|
|
711
|
+
} ? never : T extends {
|
|
712
|
+
meta: {
|
|
713
|
+
isDerived: true;
|
|
714
|
+
};
|
|
715
|
+
} ? never : T;
|
|
716
|
+
/**
|
|
717
|
+
* Infer the insert type (respects defaults and .db.auto() fields).
|
|
718
|
+
* Returns `never` for partial or derived tables to prevent insert at compile time.
|
|
719
|
+
*
|
|
720
|
+
* @example
|
|
721
|
+
* const Users = table("users", {...});
|
|
722
|
+
* type NewUser = Insert<typeof Users>;
|
|
723
|
+
*/
|
|
724
|
+
export type Insert<T extends Table<any>> = T extends {
|
|
725
|
+
meta: {
|
|
726
|
+
isPartial: true;
|
|
727
|
+
};
|
|
728
|
+
} ? never : T extends {
|
|
729
|
+
meta: {
|
|
730
|
+
isDerived: true;
|
|
731
|
+
};
|
|
732
|
+
} ? never : z.input<T["schema"]>;
|
|
733
|
+
/**
|
|
734
|
+
* Infer the update type (all fields optional, excludes primary key and insert-only fields).
|
|
735
|
+
* Returns `never` for partial or derived tables to prevent update at compile time.
|
|
736
|
+
*
|
|
737
|
+
* Note: This is a simplified version that makes all fields optional.
|
|
738
|
+
* Primary key exclusion and insert-only field exclusion require runtime enforcement.
|
|
739
|
+
*
|
|
740
|
+
* @example
|
|
741
|
+
* const Users = table("users", {...});
|
|
742
|
+
* type EditUser = Update<typeof Users>;
|
|
743
|
+
*/
|
|
744
|
+
export type Update<T extends Table<any>> = T extends {
|
|
745
|
+
meta: {
|
|
746
|
+
isPartial: true;
|
|
747
|
+
};
|
|
748
|
+
} ? never : T extends {
|
|
749
|
+
meta: {
|
|
750
|
+
isDerived: true;
|
|
751
|
+
};
|
|
752
|
+
} ? never : Partial<z.input<T["schema"]>>;
|
|
753
|
+
/**
|
|
754
|
+
* Database metadata methods available on Zod schemas.
|
|
755
|
+
* These methods are added via extendZod() and return the same schema type.
|
|
756
|
+
*/
|
|
757
|
+
export interface ZodDBMethods<Schema extends ZodType> {
|
|
758
|
+
/**
|
|
759
|
+
* Mark field as primary key.
|
|
760
|
+
* @example z.string().uuid().db.primary()
|
|
761
|
+
*/
|
|
762
|
+
primary(): Schema;
|
|
763
|
+
/**
|
|
764
|
+
* Mark field as unique.
|
|
765
|
+
* @example z.string().email().db.unique()
|
|
766
|
+
*/
|
|
767
|
+
unique(): Schema;
|
|
768
|
+
/**
|
|
769
|
+
* Create an index on this field.
|
|
770
|
+
* @example z.date().db.index()
|
|
771
|
+
*/
|
|
772
|
+
index(): Schema;
|
|
773
|
+
/**
|
|
774
|
+
* Mark field as soft delete timestamp.
|
|
775
|
+
* @example z.date().nullable().default(null).db.softDelete()
|
|
776
|
+
*/
|
|
777
|
+
softDelete(): Schema;
|
|
778
|
+
/**
|
|
779
|
+
* Define a foreign key reference with optional reverse relationship.
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* // Forward reference only
|
|
783
|
+
* authorId: z.string().uuid().db.references(Users, "author")
|
|
784
|
+
*
|
|
785
|
+
* @example
|
|
786
|
+
* // With options
|
|
787
|
+
* authorId: z.string().uuid().db.references(Users, "author", {
|
|
788
|
+
* reverseAs: "posts", // user.posts = Post[]
|
|
789
|
+
* ondelete: "cascade",
|
|
790
|
+
* })
|
|
791
|
+
*/
|
|
792
|
+
references<RefTable extends Table<any, any>, As extends string, ReverseAs extends string | undefined = undefined>(table: RefTable, as: As, options?: {
|
|
793
|
+
field?: string;
|
|
794
|
+
reverseAs?: ReverseAs;
|
|
795
|
+
onDelete?: "cascade" | "set null" | "restrict";
|
|
796
|
+
}): Schema & {
|
|
797
|
+
readonly __refTable: RefTable;
|
|
798
|
+
readonly __refAs: As;
|
|
799
|
+
readonly __refReverseAs: ReverseAs;
|
|
800
|
+
};
|
|
801
|
+
/**
|
|
802
|
+
* Encode app values to DB values (for INSERT/UPDATE).
|
|
803
|
+
* One-way transformation is fine (e.g., password hashing).
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* password: z.string().db.encode(hashPassword)
|
|
807
|
+
*
|
|
808
|
+
* @example
|
|
809
|
+
* // Bidirectional: pair with .db.decode()
|
|
810
|
+
* status: z.enum(["pending", "active"])
|
|
811
|
+
* .db.encode(s => statusMap.indexOf(s))
|
|
812
|
+
* .db.decode(i => statusMap[i])
|
|
813
|
+
*/
|
|
814
|
+
encode<TDB>(fn: (app: z.infer<Schema>) => TDB): Schema;
|
|
815
|
+
/**
|
|
816
|
+
* Decode DB values to app values (for SELECT).
|
|
817
|
+
* One-way transformation is fine.
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* legacy: z.string().db.decode(deserializeLegacyFormat)
|
|
821
|
+
*/
|
|
822
|
+
decode<TApp>(fn: (db: any) => TApp): Schema;
|
|
823
|
+
/**
|
|
824
|
+
* Shorthand for JSON encoding/decoding.
|
|
825
|
+
* Stores the value as a JSON string in the database.
|
|
826
|
+
*
|
|
827
|
+
* @example
|
|
828
|
+
* metadata: z.object({theme: z.string()}).db.json()
|
|
829
|
+
*/
|
|
830
|
+
json(): Schema;
|
|
831
|
+
/**
|
|
832
|
+
* Shorthand for CSV encoding/decoding of string arrays.
|
|
833
|
+
* Stores the array as a comma-separated string in the database.
|
|
834
|
+
*
|
|
835
|
+
* @example
|
|
836
|
+
* tags: z.array(z.string()).db.csv()
|
|
837
|
+
*/
|
|
838
|
+
csv(): Schema;
|
|
839
|
+
/**
|
|
840
|
+
* Specify explicit column type for DDL generation.
|
|
841
|
+
* Required when using custom encode/decode on objects/arrays
|
|
842
|
+
* that transform to a different storage type.
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* // Store array as CSV instead of JSON
|
|
846
|
+
* tags: z.array(z.string())
|
|
847
|
+
* .db.encode((arr) => arr.join(","))
|
|
848
|
+
* .db.decode((str) => str.split(","))
|
|
849
|
+
* .db.type("TEXT")
|
|
850
|
+
*/
|
|
851
|
+
type(columnType: string): Schema;
|
|
852
|
+
/**
|
|
853
|
+
* Set a value to apply on INSERT.
|
|
854
|
+
*
|
|
855
|
+
* Three forms:
|
|
856
|
+
* - Tagged template: .db.inserted`CURRENT_TIMESTAMP`
|
|
857
|
+
* - Symbol: .db.inserted(NOW)
|
|
858
|
+
* - Function: .db.inserted(() => "draft")
|
|
859
|
+
*
|
|
860
|
+
* Field becomes optional for insert.
|
|
861
|
+
*
|
|
862
|
+
* **Note:** SQL expressions (tagged templates and symbols) bypass encode/decode
|
|
863
|
+
* since they're executed by the database, not the application. Use function
|
|
864
|
+
* form if you need encoding applied.
|
|
865
|
+
*
|
|
866
|
+
* **Note:** Interpolated values in tagged templates are parameterized but not
|
|
867
|
+
* schema-validated. Ensure values are appropriate for the column type.
|
|
868
|
+
*
|
|
869
|
+
* @example
|
|
870
|
+
* createdAt: z.date().db.inserted(NOW)
|
|
871
|
+
* token: z.string().db.inserted(() => crypto.randomUUID())
|
|
872
|
+
* slug: z.string().db.inserted`LOWER(name)`
|
|
873
|
+
*/
|
|
874
|
+
inserted(value: import("./database.js").SQLBuiltin | (() => z.infer<Schema>)): Schema;
|
|
875
|
+
inserted(strings: TemplateStringsArray, ...values: unknown[]): Schema;
|
|
876
|
+
/**
|
|
877
|
+
* Set a value to apply on UPDATE only.
|
|
878
|
+
*
|
|
879
|
+
* Same forms as inserted(). See inserted() for notes on codec bypass
|
|
880
|
+
* and template parameter validation.
|
|
881
|
+
*
|
|
882
|
+
* Field becomes optional for update operations.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* modifiedAt: z.date().db.updated(NOW)
|
|
886
|
+
* lastModified: z.date().db.updated(() => new Date())
|
|
887
|
+
*/
|
|
888
|
+
updated(value: import("./database.js").SQLBuiltin | (() => z.infer<Schema>)): Schema;
|
|
889
|
+
updated(strings: TemplateStringsArray, ...values: unknown[]): Schema;
|
|
890
|
+
/**
|
|
891
|
+
* Set a value to apply on both INSERT and UPDATE.
|
|
892
|
+
*
|
|
893
|
+
* Same forms as inserted(). See inserted() for notes on codec bypass
|
|
894
|
+
* and template parameter validation.
|
|
895
|
+
*
|
|
896
|
+
* Field becomes optional for insert/update.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* updatedAt: z.date().db.upserted(NOW)
|
|
900
|
+
* lastModified: z.date().db.upserted(() => new Date())
|
|
901
|
+
*/
|
|
902
|
+
upserted(value: import("./database.js").SQLBuiltin | (() => z.infer<Schema>)): Schema;
|
|
903
|
+
upserted(strings: TemplateStringsArray, ...values: unknown[]): Schema;
|
|
904
|
+
/**
|
|
905
|
+
* Auto-generate value on insert based on field type.
|
|
906
|
+
*
|
|
907
|
+
* Type-aware behavior:
|
|
908
|
+
* - `z.string().uuid()` → generates UUID via `crypto.randomUUID()`
|
|
909
|
+
* - `z.number().int()` → auto-increment (database-side)
|
|
910
|
+
* - `z.date()` → current timestamp via NOW
|
|
911
|
+
*
|
|
912
|
+
* Field becomes optional for insert.
|
|
913
|
+
*
|
|
914
|
+
* @example
|
|
915
|
+
* id: z.string().uuid().db.primary().db.auto()
|
|
916
|
+
* // → crypto.randomUUID() on insert
|
|
917
|
+
*
|
|
918
|
+
* @example
|
|
919
|
+
* id: z.number().int().db.primary().db.auto()
|
|
920
|
+
* // → auto-increment
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* createdAt: z.date().db.auto()
|
|
924
|
+
* // → NOW on insert
|
|
925
|
+
*/
|
|
926
|
+
auto(): Schema;
|
|
927
|
+
}
|
|
928
|
+
declare module "zod" {
|
|
929
|
+
interface ZodType<out Output, out Input, out Internals> {
|
|
930
|
+
readonly db: ZodDBMethods<this>;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Infer the database field type from a Zod schema.
|
|
935
|
+
* Used by encodeData() and decodeData() to determine type-specific handling.
|
|
936
|
+
*
|
|
937
|
+
* @param schema - The Zod schema for the field
|
|
938
|
+
* @returns The field type: "text", "integer", "real", "boolean", "datetime", "json"
|
|
939
|
+
*/
|
|
940
|
+
export declare function inferFieldType(schema: z.ZodTypeAny): string;
|
|
941
|
+
/**
|
|
942
|
+
* Minimal interface for driver decoding capability.
|
|
943
|
+
* Defined here to avoid circular imports from database.ts.
|
|
944
|
+
*/
|
|
945
|
+
export interface DriverDecoder {
|
|
946
|
+
decodeValue?(value: unknown, fieldType: string): unknown;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Decode database result data into proper JS types using table schema.
|
|
950
|
+
*
|
|
951
|
+
* Priority order:
|
|
952
|
+
* 1. Custom field-level .db.decode() (always wins)
|
|
953
|
+
* 2. Driver.decodeValue() (dialect-specific)
|
|
954
|
+
* 3. Auto-decode fallback (JSON.parse, 0/1→boolean, string→Date)
|
|
955
|
+
*
|
|
956
|
+
* @param table - The table/view definition
|
|
957
|
+
* @param data - The raw database row
|
|
958
|
+
* @param driver - Optional driver for dialect-specific decoding
|
|
959
|
+
*/
|
|
960
|
+
export declare function decodeData<T extends Queryable<any>>(table: T, data: Record<string, unknown> | null, driver?: DriverDecoder): Record<string, unknown> | null;
|
|
961
|
+
export {};
|