zod-sqlite 1.0.4 → 1.0.5

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/README.md CHANGED
@@ -1,10 +1,9 @@
1
1
  <div align="center">
2
2
  <h1>Zod-SQLite</h1>
3
- <p><strong>Generate type-safe SQLite table schemas from Zod validation schemas. Define your database structure once using Zod, and automatically generate both SQL CREATE TABLE statements and runtime validation schemas with full TypeScript type inference.</strong></p>
3
+ <p>Generate type-safe SQLite table schemas from Zod validation schemas. Define your database structure once using Zod, and automatically generate both SQL CREATE TABLE statements and runtime validation schemas with full TypeScript type inference.</p>
4
4
  <p>
5
- <a href="https://github.com/favorodera/zod-sqlite/blob/main/LICENSE"><img src="https://img.shields.io/github/license/favorodera/zod-sqlite.svg?style=plastic&label=License&color=blue" alt="License"></a>
6
5
  <a href="https://github.com/favorodera/zod-sqlite/stargazers"><img src="https://img.shields.io/github/stars/favorodera/zod-sqlite.svg?style=plastic&label=Stars&color=blue" alt="GitHub Stars"></a>
7
- <a href="https://npmx.dev/package/zod-sqlite"><img src="https://img.shields.io/npm/dt/zod-sqlite.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
6
+ <a href="https://npmx.dev/package/zod-sqlite"><img src="https://img.shields.io/npm/dm/zod-sqlite.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
8
7
  <a href="https://npmx.dev/package/zod-sqlite"><img src="https://img.shields.io/npm/v/zod-sqlite.svg?style=plastic&label=Version&color=blue" alt="NPM Version"></a>
9
8
  </p>
10
9
  </div>
@@ -76,13 +75,15 @@ const users = createTable({
76
75
 
77
76
  // Use the generated SQL
78
77
  console.log(users.table)
79
- // CREATE TABLE users (
80
- // id INTEGER NOT NULL,
81
- // email TEXT NOT NULL,
82
- // username TEXT NOT NULL CHECK(length(username) >= 3 AND length(username) <= 20),
83
- // created_at DATE NOT NULL DEFAULT '2026-01-06T...',
84
- // PRIMARY KEY (id)
85
- // );
78
+ /*
79
+ * CREATE TABLE users (
80
+ * id INTEGER NOT NULL,
81
+ * email TEXT NOT NULL,
82
+ * username TEXT NOT NULL CHECK(length(username) >= 3 AND length(username) <= 20),
83
+ * created_at DATE NOT NULL DEFAULT '2026-01-06T...',
84
+ * PRIMARY KEY (id)
85
+ * );
86
+ */
86
87
 
87
88
  console.log(users.indexes[0])
88
89
  // CREATE UNIQUE INDEX idx_users_email ON users (email);
@@ -170,10 +171,10 @@ Configuration object for table creation.
170
171
 
171
172
  ```typescript
172
173
  interface TableConfig {
173
- columns: Array<ColumnConfig> // Array of column definitions
174
- indexes?: Array<IndexConfig> // Optional index configurations
174
+ columns: ColumnConfig[] // Array of column definitions
175
+ indexes?: IndexConfig[] // Optional index configurations
175
176
  name: string // Table name
176
- primaryKeys: Array<string> // Column names forming primary key
177
+ primaryKeys: string[] // Column names forming primary key
177
178
  }
178
179
  ```
179
180
 
@@ -195,19 +196,19 @@ interface ColumnConfig {
195
196
  Foreign key constraint configuration.
196
197
 
197
198
  ```typescript
198
- interface ForeignKeyReference {
199
- column: string // Referenced column name
200
- onDelete?: ForeignKeyAction // Action on parent deletion
201
- onUpdate?: ForeignKeyAction // Action on parent update
202
- table: string // Referenced table name
203
- }
204
-
205
199
  type ForeignKeyAction
206
200
  = | 'CASCADE'
207
201
  | 'NO ACTION'
208
202
  | 'RESTRICT'
209
203
  | 'SET DEFAULT'
210
- | 'SET NULL'
204
+ | 'SET NULL'
205
+
206
+ interface ForeignKeyReference {
207
+ column: string // Referenced column name
208
+ onDelete?: ForeignKeyAction // Action on parent deletion
209
+ onUpdate?: ForeignKeyAction // Action on parent update
210
+ table: string // Referenced table name
211
+ }
211
212
  ```
212
213
 
213
214
  ### IndexConfig
@@ -216,7 +217,7 @@ Index configuration for query optimization.
216
217
 
217
218
  ```typescript
218
219
  interface IndexConfig {
219
- columns: Array<string> // Indexed column names
220
+ columns: string[] // Indexed column names
220
221
  name: string // Index name
221
222
  unique?: boolean // Whether this is a unique index
222
223
  where?: string // Optional WHERE clause for partial index
@@ -949,8 +950,10 @@ const tags = JSON.parse(result.tags) // ['tech', 'news']
949
950
 
950
951
  **Recommendation:** For better queryability, consider separate tables for array data:
951
952
  ```typescript
952
- // Instead of storing tags as JSON array
953
- // Use a junction table: post_tags (post_id, tag_id)
953
+ /*
954
+ * Instead of storing tags as JSON array
955
+ * Use a junction table: post_tags (post_id, tag_id)
956
+ */
954
957
  ```
955
958
 
956
959
  ### Date Handling
package/dist/index.d.mts CHANGED
@@ -1,22 +1,134 @@
1
1
  import * as zod from "zod/v4/core";
2
- //#region src/types.d.ts
3
2
  /**
4
- * SQLite native column types.
3
+ * Database column configuration.
5
4
  *
6
- * SQLite uses a dynamic type system with these five storage classes.
7
- * Column values are stored in one of these formats, and SQLite performs
8
- * type conversions as needed during operations.
9
- * @see https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
10
- */
11
- type SQLiteType = 'BLOB' | 'INTEGER' | 'NULL' | 'REAL' | 'TEXT';
12
- /**
13
- * SQLite supported column types.
5
+ * Defines a single column in a table, including its name, type (via Zod schema),
6
+ * validation rules, and optional constraints like uniqueness and foreign keys.
14
7
  *
15
- * These types are supported by SQLite but not as native storage classes.
16
- * They are often used as aliases for other types or for specific purposes.
17
- * @see https://www.sqlite.org/datatype3.html#type_affinity
8
+ * **Column definition workflow:**
9
+ * 1. Zod schema is analyzed to determine SQLite type (TEXT, INTEGER, etc.)
10
+ * 2. Zod schema provides TypeScript type safety and runtime validation
11
+ * 3. Constraints (unique, foreign key) are applied at the database level
12
+ * @template TName - Literal string type for the column name
13
+ * @template TSchema - Zod type for the column schema
14
+ * @example
15
+ * ```ts
16
+ * // Simple text column
17
+ * {
18
+ * name: 'email',
19
+ * schema: z.email()
20
+ * }
21
+ *
22
+ * // Unique column with validation
23
+ * {
24
+ * name: 'username',
25
+ * schema: z.string().min(3).max(20),
26
+ * unique: true
27
+ * }
28
+ *
29
+ * // Foreign key column
30
+ * {
31
+ * name: 'author_id',
32
+ * schema: z.int(),
33
+ * references: {
34
+ * table: 'users',
35
+ * column: 'id',
36
+ * onDelete: 'CASCADE'
37
+ * }
38
+ * }
39
+ *
40
+ * // Optional column with default
41
+ * {
42
+ * name: 'status',
43
+ * schema: z.enum(['draft', 'published']).default('draft')
44
+ * }
45
+ * ```
18
46
  */
19
- type SQLiteSupportType = 'BIGINT' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'FLOAT' | 'NUMERIC';
47
+ export interface ColumnConfig<TName extends string = string, TSchema extends zod.$ZodType = zod.$ZodType> {
48
+ /**
49
+ * Name of the column.
50
+ *
51
+ * Used in CREATE TABLE statement and all SQL queries.
52
+ * Should use snake_case by convention.
53
+ * @example 'user_id'
54
+ * @example 'created_at'
55
+ */
56
+ name: TName;
57
+ /**
58
+ * Foreign key reference configuration.
59
+ *
60
+ * Creates a constraint linking this column to a column in another table.
61
+ * Ensures referential integrity by preventing orphaned records.
62
+ *
63
+ * **Requirements:**
64
+ * - Referenced column should typically be a PRIMARY KEY or UNIQUE
65
+ * - Types must be compatible between the two columns
66
+ * - Foreign keys must be enabled: `PRAGMA foreign_keys = ON`
67
+ *
68
+ * **Composite foreign keys** (multiple columns referencing multiple columns)
69
+ * cannot be defined here - they must be defined at the table level as a
70
+ * separate constraint.
71
+ * @example
72
+ * ```ts
73
+ * {
74
+ * name: 'user_id',
75
+ * schema: z.int(),
76
+ * references: {
77
+ * table: 'users',
78
+ * column: 'id',
79
+ * onDelete: 'CASCADE'
80
+ * }
81
+ * }
82
+ * ```
83
+ */
84
+ references?: ForeignKeyReference;
85
+ /**
86
+ * Zod schema defining the column's type, validation, and constraints.
87
+ *
88
+ * The Zod schema serves dual purposes:
89
+ * 1. **Database level**: Mapped to SQLite type (TEXT, INTEGER, REAL, BLOB, NULL)
90
+ * 2. **Application level**: Provides TypeScript types and runtime validation
91
+ *
92
+ * **Zod to SQLite type mapping:**
93
+ * - `z.string()`, `z.enum()`, `z.literal()`, `z.date()` → TEXT
94
+ * - `z.number()` → REAL (or INTEGER if `.int()` is used)
95
+ * - `z.bigint()`, `z.boolean()` → INTEGER
96
+ * - `z.custom()` for file/blob → BLOB
97
+ * - `z.null()`, `z.undefined()` → NULL
98
+ *
99
+ * **Zod features mapped to SQL:**
100
+ * - `.optional()`, `.nullable()` → column allows NULL
101
+ * - `.default(value)` → DEFAULT clause in SQL
102
+ * - `.min()`, `.max()`, `.email()`, etc. → runtime validation only
103
+ * @example z.string() - TEXT column
104
+ * @example z.int() - INTEGER column
105
+ * @example z.string().nullable() - TEXT column that allows NULL
106
+ * @example z.boolean().default(false) - INTEGER with DEFAULT 0
107
+ */
108
+ schema: TSchema;
109
+ /**
110
+ * Whether column values must be unique across all rows.
111
+ *
112
+ * When true, adds a UNIQUE constraint to the column. Any attempt to
113
+ * INSERT or UPDATE a duplicate value will fail.
114
+ *
115
+ * **Note**: This is different from PRIMARY KEY:
116
+ * - UNIQUE columns can be NULL (unless also NOT NULL)
117
+ * - A table can have multiple UNIQUE columns
118
+ * - PRIMARY KEY implies UNIQUE + NOT NULL
119
+ * @default false
120
+ * @example
121
+ * ```ts
122
+ * // Unique email address
123
+ * {
124
+ * name: 'email',
125
+ * schema: z.email(),
126
+ * unique: true
127
+ * }
128
+ * ```
129
+ */
130
+ unique?: boolean;
131
+ }
20
132
  /**
21
133
  * Foreign key constraint actions for ON DELETE and ON UPDATE clauses.
22
134
  *
@@ -50,7 +162,7 @@ type SQLiteSupportType = 'BIGINT' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'FLOAT' |
50
162
  * }
51
163
  * ```
52
164
  */
53
- type ForeignKeyAction = 'CASCADE' | 'NO ACTION' | 'RESTRICT' | 'SET DEFAULT' | 'SET NULL';
165
+ export type ForeignKeyAction = 'CASCADE' | 'NO ACTION' | 'RESTRICT' | 'SET DEFAULT' | 'SET NULL';
54
166
  /**
55
167
  * Foreign key reference configuration.
56
168
  *
@@ -80,13 +192,7 @@ type ForeignKeyAction = 'CASCADE' | 'NO ACTION' | 'RESTRICT' | 'SET DEFAULT' | '
80
192
  * }
81
193
  * ```
82
194
  */
83
- interface ForeignKeyReference {
84
- /**
85
- * Name of the referenced table.
86
- *
87
- * This is the parent table that contains the column being referenced.
88
- */
89
- table: string;
195
+ export interface ForeignKeyReference {
90
196
  /**
91
197
  * Name of the referenced column in the parent table.
92
198
  *
@@ -109,6 +215,12 @@ interface ForeignKeyReference {
109
215
  * @default 'NO ACTION'
110
216
  */
111
217
  onUpdate?: ForeignKeyAction;
218
+ /**
219
+ * Name of the referenced table.
220
+ *
221
+ * This is the parent table that contains the column being referenced.
222
+ */
223
+ table: string;
112
224
  }
113
225
  /**
114
226
  * Database index configuration.
@@ -152,7 +264,16 @@ interface ForeignKeyReference {
152
264
  * }
153
265
  * ```
154
266
  */
155
- interface IndexConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$ZodType>> = ReadonlyArray<ColumnConfig<string, zod.$ZodType>>> {
267
+ export interface IndexConfig<TColumns extends readonly ColumnConfig<string, zod.$ZodType>[] = readonly ColumnConfig<string, zod.$ZodType>[]> {
268
+ /**
269
+ * Column names to include in the index.
270
+ *
271
+ * For composite indexes, the order matters: queries that filter on the
272
+ * first columns benefit most from the index.
273
+ * @example ['email'] - single column
274
+ * @example ['last_name', 'first_name'] - composite index
275
+ */
276
+ columns: readonly TColumns[number]['name'][];
156
277
  /**
157
278
  * Unique name for the index.
158
279
  *
@@ -162,15 +283,6 @@ interface IndexConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$Z
162
283
  * @example 'idx_posts_author_created'
163
284
  */
164
285
  name: string;
165
- /**
166
- * Column names to include in the index.
167
- *
168
- * For composite indexes, the order matters: queries that filter on the
169
- * first columns benefit most from the index.
170
- * @example ['email'] - single column
171
- * @example ['last_name', 'first_name'] - composite index
172
- */
173
- columns: ReadonlyArray<TColumns[number]['name']>;
174
286
  /**
175
287
  * Whether this is a unique index.
176
288
  *
@@ -195,155 +307,27 @@ interface IndexConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$Z
195
307
  where?: string;
196
308
  }
197
309
  /**
198
- * Database column configuration.
199
- *
200
- * Defines a single column in a table, including its name, type (via Zod schema),
201
- * validation rules, and optional constraints like uniqueness and foreign keys.
202
- *
203
- * **Column definition workflow:**
204
- * 1. Zod schema is analyzed to determine SQLite type (TEXT, INTEGER, etc.)
205
- * 2. Zod schema provides TypeScript type safety and runtime validation
206
- * 3. Constraints (unique, foreign key) are applied at the database level
207
- * @template TName - Literal string type for the column name
208
- * @template TSchema - Zod type for the column schema
209
- * @example
210
- * ```ts
211
- * // Simple text column
212
- * {
213
- * name: 'email',
214
- * schema: z.email()
215
- * }
216
- *
217
- * // Unique column with validation
218
- * {
219
- * name: 'username',
220
- * schema: z.string().min(3).max(20),
221
- * unique: true
222
- * }
310
+ * SQLite supported column types.
223
311
  *
224
- * // Foreign key column
225
- * {
226
- * name: 'author_id',
227
- * schema: z.int(),
228
- * references: {
229
- * table: 'users',
230
- * column: 'id',
231
- * onDelete: 'CASCADE'
232
- * }
233
- * }
312
+ * These types are supported by SQLite but not as native storage classes.
313
+ * They are often used as aliases for other types or for specific purposes.
314
+ * @see https://www.sqlite.org/datatype3.html#type_affinity
315
+ */
316
+ export type SQLiteSupportType = 'BIGINT' | 'BOOLEAN' | 'DATE' | 'DATETIME' | 'FLOAT' | 'NUMERIC';
317
+ /**
318
+ * SQLite native column types.
234
319
  *
235
- * // Optional column with default
236
- * {
237
- * name: 'status',
238
- * schema: z.enum(['draft', 'published']).default('draft')
239
- * }
240
- * ```
320
+ * SQLite uses a dynamic type system with these five storage classes.
321
+ * Column values are stored in one of these formats, and SQLite performs
322
+ * type conversions as needed during operations.
323
+ * @see https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
241
324
  */
242
- interface ColumnConfig<TName extends string = string, TSchema extends zod.$ZodType = zod.$ZodType> {
243
- /**
244
- * Name of the column.
245
- *
246
- * Used in CREATE TABLE statement and all SQL queries.
247
- * Should use snake_case by convention.
248
- * @example 'user_id'
249
- * @example 'created_at'
250
- */
251
- name: TName;
252
- /**
253
- * Zod schema defining the column's type, validation, and constraints.
254
- *
255
- * The Zod schema serves dual purposes:
256
- * 1. **Database level**: Mapped to SQLite type (TEXT, INTEGER, REAL, BLOB, NULL)
257
- * 2. **Application level**: Provides TypeScript types and runtime validation
258
- *
259
- * **Zod to SQLite type mapping:**
260
- * - `z.string()`, `z.enum()`, `z.literal()`, `z.date()` → TEXT
261
- * - `z.number()` → REAL (or INTEGER if `.int()` is used)
262
- * - `z.bigint()`, `z.boolean()` → INTEGER
263
- * - `z.custom()` for file/blob → BLOB
264
- * - `z.null()`, `z.undefined()` → NULL
265
- *
266
- * **Zod features mapped to SQL:**
267
- * - `.optional()`, `.nullable()` → column allows NULL
268
- * - `.default(value)` → DEFAULT clause in SQL
269
- * - `.min()`, `.max()`, `.email()`, etc. → runtime validation only
270
- * @example z.string() - TEXT column
271
- * @example z.int() - INTEGER column
272
- * @example z.string().nullable() - TEXT column that allows NULL
273
- * @example z.boolean().default(false) - INTEGER with DEFAULT 0
274
- */
275
- schema: TSchema;
276
- /**
277
- * Whether column values must be unique across all rows.
278
- *
279
- * When true, adds a UNIQUE constraint to the column. Any attempt to
280
- * INSERT or UPDATE a duplicate value will fail.
281
- *
282
- * **Note**: This is different from PRIMARY KEY:
283
- * - UNIQUE columns can be NULL (unless also NOT NULL)
284
- * - A table can have multiple UNIQUE columns
285
- * - PRIMARY KEY implies UNIQUE + NOT NULL
286
- * @default false
287
- * @example
288
- * ```ts
289
- * // Unique email address
290
- * {
291
- * name: 'email',
292
- * schema: z.email(),
293
- * unique: true
294
- * }
295
- * ```
296
- */
297
- unique?: boolean;
298
- /**
299
- * Foreign key reference configuration.
300
- *
301
- * Creates a constraint linking this column to a column in another table.
302
- * Ensures referential integrity by preventing orphaned records.
303
- *
304
- * **Requirements:**
305
- * - Referenced column should typically be a PRIMARY KEY or UNIQUE
306
- * - Types must be compatible between the two columns
307
- * - Foreign keys must be enabled: `PRAGMA foreign_keys = ON`
308
- *
309
- * **Composite foreign keys** (multiple columns referencing multiple columns)
310
- * cannot be defined here - they must be defined at the table level as a
311
- * separate constraint.
312
- * @example
313
- * ```ts
314
- * {
315
- * name: 'user_id',
316
- * schema: z.int(),
317
- * references: {
318
- * table: 'users',
319
- * column: 'id',
320
- * onDelete: 'CASCADE'
321
- * }
322
- * }
323
- * ```
324
- */
325
- references?: ForeignKeyReference;
326
- }
325
+ export type SQLiteType = 'BLOB' | 'INTEGER' | 'NULL' | 'REAL' | 'TEXT';
327
326
  /**
328
327
  * Complete database table configuration.
329
328
  * @template TColumns - An array of column configuration objects.
330
329
  */
331
- interface TableConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$ZodType>> = ReadonlyArray<ColumnConfig<string, zod.$ZodType>>> {
332
- /**
333
- * Name of the table.
334
- *
335
- * Used in CREATE TABLE statement and all SQL queries.
336
- * Should use snake_case by convention (e.g., 'user_profiles', 'order_items').
337
- *
338
- * **Naming conventions:**
339
- * - Use plural nouns for tables representing collections (e.g., 'users', 'posts')
340
- * - Use singular nouns for junction tables (e.g., 'user_role', 'post_tag')
341
- * - Avoid SQL reserved keywords (e.g., 'order' → 'orders' or 'user_order')
342
- * @example 'users'
343
- * @example 'blog_posts'
344
- * @example 'user_sessions'
345
- */
346
- name: string;
330
+ export interface TableConfig<TColumns extends readonly ColumnConfig<string, zod.$ZodType>[] = readonly ColumnConfig<string, zod.$ZodType>[]> {
347
331
  /**
348
332
  * Array of column configurations defining the table structure.
349
333
  *
@@ -366,35 +350,6 @@ interface TableConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$Z
366
350
  * ```
367
351
  */
368
352
  columns: TColumns;
369
- /**
370
- * Column name(s) that form the primary key.
371
- *
372
- * The primary key uniquely identifies each row in the table and cannot
373
- * contain NULL values. Always provided as an array, even for single-column
374
- * primary keys.
375
- *
376
- * **Single column primary key:**
377
- * - Use an array with one element (e.g., `['id']`)
378
- * - Typically an auto-incrementing integer
379
- * - Most common pattern for standard entity tables
380
- *
381
- * **Composite primary key:**
382
- * - Use an array with multiple elements (e.g., `['user_id', 'post_id']`)
383
- * - Common in junction tables for many-to-many relationships
384
- * - Order matters: most specific/selective column should come first
385
- * - The combination of all columns must be unique
386
- *
387
- * **Notes:**
388
- * - Every table should have a primary key
389
- * - Primary key columns are automatically indexed by SQLite
390
- * - Primary key implies UNIQUE + NOT NULL constraints on all columns
391
- * - Cannot be changed after table creation (requires table rebuild)
392
- * @example ['id'] - single column primary key
393
- * @example ['user_id', 'role_id'] - composite primary key for junction table
394
- * @example ['tenant_id', 'record_id'] - composite key for multi-tenant data
395
- * @example ['country_code', 'postal_code'] - composite key for location data
396
- */
397
- primaryKeys: ReadonlyArray<TColumns[number]['name']>;
398
353
  /**
399
354
  * Optional array of index configurations for query optimization.
400
355
  *
@@ -433,10 +388,52 @@ interface TableConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$Z
433
388
  * ]
434
389
  * ```
435
390
  */
436
- indexes?: Array<IndexConfig<TColumns>>;
391
+ indexes?: IndexConfig<TColumns>[];
392
+ /**
393
+ * Name of the table.
394
+ *
395
+ * Used in CREATE TABLE statement and all SQL queries.
396
+ * Should use snake_case by convention (e.g., 'user_profiles', 'order_items').
397
+ *
398
+ * **Naming conventions:**
399
+ * - Use plural nouns for tables representing collections (e.g., 'users', 'posts')
400
+ * - Use singular nouns for junction tables (e.g., 'user_role', 'post_tag')
401
+ * - Avoid SQL reserved keywords (e.g., 'order' → 'orders' or 'user_order')
402
+ * @example 'users'
403
+ * @example 'blog_posts'
404
+ * @example 'user_sessions'
405
+ */
406
+ name: string;
407
+ /**
408
+ * Column name(s) that form the primary key.
409
+ *
410
+ * The primary key uniquely identifies each row in the table and cannot
411
+ * contain NULL values. Always provided as an array, even for single-column
412
+ * primary keys.
413
+ *
414
+ * **Single column primary key:**
415
+ * - Use an array with one element (e.g., `['id']`)
416
+ * - Typically an auto-incrementing integer
417
+ * - Most common pattern for standard entity tables
418
+ *
419
+ * **Composite primary key:**
420
+ * - Use an array with multiple elements (e.g., `['user_id', 'post_id']`)
421
+ * - Common in junction tables for many-to-many relationships
422
+ * - Order matters: most specific/selective column should come first
423
+ * - The combination of all columns must be unique
424
+ *
425
+ * **Notes:**
426
+ * - Every table should have a primary key
427
+ * - Primary key columns are automatically indexed by SQLite
428
+ * - Primary key implies UNIQUE + NOT NULL constraints on all columns
429
+ * - Cannot be changed after table creation (requires table rebuild)
430
+ * @example ['id'] - single column primary key
431
+ * @example ['user_id', 'role_id'] - composite primary key for junction table
432
+ * @example ['tenant_id', 'record_id'] - composite key for multi-tenant data
433
+ * @example ['country_code', 'postal_code'] - composite key for location data
434
+ */
435
+ primaryKeys: readonly TColumns[number]['name'][];
437
436
  }
438
- //#endregion
439
- //#region src/table.d.ts
440
437
  /**
441
438
  * Creates a table definition and Zod schema.
442
439
  *
@@ -449,12 +446,10 @@ interface TableConfig<TColumns extends ReadonlyArray<ColumnConfig<string, zod.$Z
449
446
  * @param config Table configuration object
450
447
  * @returns Object containing SQL statements and Zod schema
451
448
  */
452
- declare function createTable<const TColumns extends ReadonlyArray<ColumnConfig<any, any>>>(config: TableConfig<TColumns>): {
449
+ export declare function createTable<const TColumns extends readonly ColumnConfig<any, any>[]>(config: TableConfig<TColumns>): {
453
450
  indexes: string[];
454
451
  schema: import("zod").ZodObject<{ [K in TColumns[number]["name"]]: Extract<TColumns[number], {
455
452
  name: K;
456
453
  }>["schema"]; }, import("zod/v4/core").$strip>;
457
454
  table: string;
458
- };
459
- //#endregion
460
- export { ColumnConfig, ForeignKeyAction, ForeignKeyReference, IndexConfig, SQLiteSupportType, SQLiteType, TableConfig, createTable };
455
+ };
package/dist/index.mjs CHANGED
@@ -1,283 +1,4 @@
1
- import { z } from "zod";
2
- //#region src/utils/format-check-constraint.ts
3
- /**
4
- * Generates formatted CHECK constraints for validatable columns.
5
- *
6
- * Supports:
7
- * - `z.enum()`: CHECK(col IN ('a', 'b'))
8
- * - `z.literal()`: CHECK(col = 'val')
9
- * - `z.number().min/max()`: CHECK(col >= min AND col <= max)
10
- * - `z.string().min/max()`: CHECK(length(col) >= min)
11
- * @param name Column name
12
- * @param schema Zod schema
13
- * @returns SQL CHECK constraint string or null
14
- */
15
- function formatCheckConstraint(name, schema) {
16
- let currentSchema = schema;
17
- while (true) {
18
- const def = currentSchema._zod.def;
19
- if (def.type === "nullable" || def.type === "optional" || def.type === "default") currentSchema = def.innerType;
20
- else break;
21
- }
22
- const definition = currentSchema._zod.def;
23
- switch (definition.type) {
24
- case "enum": {
25
- const entries = definition.entries;
26
- return `CHECK(${name} IN (${Object.values(entries).map((value) => `'${String(value).replaceAll("'", "''")}'`).join(", ")}))`;
27
- }
28
- case "literal": {
29
- const values = definition.values;
30
- if (values.length === 1) {
31
- const value = values[0];
32
- if (typeof value === "string") return `CHECK(${name} = '${value.replaceAll("'", "''")}')`;
33
- return `CHECK(${name} = ${value})`;
34
- }
35
- return `CHECK(${name} IN (${values.map((value) => {
36
- if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
37
- return String(value);
38
- }).join(", ")}))`;
39
- }
40
- case "number": {
41
- const checks = definition.checks;
42
- const constraints = [];
43
- if (!checks) return;
44
- for (const check of checks) {
45
- const checkParameters = check._zod.def.check;
46
- const checkValue = check._zod.def.value;
47
- switch (checkParameters) {
48
- case "greater_than":
49
- constraints.push(`${name} >= ${checkValue}`);
50
- break;
51
- case "less_than": constraints.push(`${name} <= ${checkValue}`);
52
- }
53
- }
54
- return constraints.length > 0 ? `CHECK(${constraints.join(" AND ")})` : void 0;
55
- }
56
- case "string": {
57
- const checks = definition.checks;
58
- const constraints = [];
59
- if (!checks) return;
60
- for (const check of checks) {
61
- const checkParameters = check._zod.def.check;
62
- const checkDefinition = check._zod.def;
63
- switch (checkParameters) {
64
- case "length_equals":
65
- constraints.push(`length(${name}) = ${checkDefinition.length}`);
66
- break;
67
- case "max_length":
68
- constraints.push(`length(${name}) <= ${checkDefinition.maximum}`);
69
- break;
70
- case "min_length": constraints.push(`length(${name}) >= ${checkDefinition.minimum}`);
71
- }
72
- }
73
- return constraints.length > 0 ? `CHECK(${constraints.join(" AND ")})` : void 0;
74
- }
75
- case "union": {
76
- const options = definition.options;
77
- if (!options.every((option) => option._zod.def.type === "literal")) return;
78
- const values = [];
79
- for (const option of options) values.push(...option._zod.def.values);
80
- return `CHECK(${name} IN (${values.map((value) => {
81
- if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
82
- return String(value);
83
- }).join(", ")}))`;
84
- }
85
- }
86
- }
87
- //#endregion
88
- //#region src/utils/format-default-value.ts
89
- /**
90
- * Formats a default value for SQL.
91
- *
92
- * Handles string quoting and type conversions for DEFAULT clauses.
93
- * @param value The default value
94
- * @param SQLiteType Target column type
95
- * @returns Formatted SQL default string (e.g., "DEFAULT 'val'", "DEFAULT 42")
96
- */
97
- function formatDefaultValue(value, SQLiteType) {
98
- switch (SQLiteType) {
99
- case "TEXT": return `DEFAULT '${String(value).replaceAll("'", "''")}'`;
100
- case "INTEGER":
101
- case "REAL": return `DEFAULT ${Number(value)}`;
102
- case "BOOLEAN": return `DEFAULT ${value ? 1 : 0}`;
103
- case "NULL": return "DEFAULT NULL";
104
- default: return `DEFAULT '${value}'`;
105
- }
106
- }
107
- //#endregion
108
- //#region src/utils/map-zod-type-to-sqlite.ts
109
- /**
110
- * Maps a Zod type to an SQLite column type.
111
- *
112
- * analyzes the Zod schema type and optionally its format (e.g., z.email())
113
- * to determine the most appropriate SQLite storage class or supported type.
114
- * @param zodType Zod type string (e.g., 'string', 'number')
115
- * @param schema The actual Zod schema instance
116
- * @returns SQLite column type
117
- */
118
- function mapZodTypeToSQLite(zodType, schema) {
119
- switch (zodType) {
120
- case "string": switch (schema._zod.def.format) {
121
- case "date": return "DATE";
122
- case "datetime": return "DATETIME";
123
- case "duration": return "INTEGER";
124
- default: return "TEXT";
125
- }
126
- case "enum":
127
- case "literal":
128
- case "array":
129
- case "template_literal":
130
- case "object": return "TEXT";
131
- case "date": return "DATE";
132
- case "number": switch (schema._zod.def.format) {
133
- case "safeint":
134
- case "uint32":
135
- case "int32": return "INTEGER";
136
- case "float32":
137
- case "float64": return "FLOAT";
138
- default: return "REAL";
139
- }
140
- case "boolean": return "BOOLEAN";
141
- case "bigint": return "BIGINT";
142
- case "null":
143
- case "undefined": return "NULL";
144
- case "file": return "BLOB";
145
- default: return "TEXT";
146
- }
147
- }
148
- //#endregion
149
- //#region src/utils/zod-to-sqlite.ts
150
- /**
151
- * Analyzes a Zod schema to extract SQLite metadata.
152
- *
153
- * Unwraps optional/nullable/default layers to find the underlying core type
154
- * and extracts metadata like nullability and default values.
155
- * @param schema Zod schema to analyze
156
- * @returns Object containing SQLite type, nullability, default value, and inner schema
157
- */
158
- function zodToSQLite(schema) {
159
- let nullable = false;
160
- let defaultValue;
161
- let currentSchema = schema;
162
- while (true) {
163
- const definition = currentSchema._zod.def;
164
- const zodType = definition.type;
165
- if (zodType === "nullable" || zodType === "optional") {
166
- nullable = true;
167
- currentSchema = definition.innerType;
168
- } else if (zodType === "default") {
169
- defaultValue = typeof definition.defaultValue === "function" ? definition.defaultValue() : definition.defaultValue;
170
- currentSchema = definition.innerType;
171
- } else break;
172
- }
173
- const mainType = currentSchema._zod.def.type;
174
- const SQLiteType = mapZodTypeToSQLite(mainType, currentSchema);
175
- return {
176
- defaultValue,
177
- nullable,
178
- schema,
179
- SQLiteType
180
- };
181
- }
182
- //#endregion
183
- //#region src/column-definitions.ts
184
- /**
185
- * Builds the SQL definition for a single column.
186
- *
187
- * Combines name, type, and constraints into a valid SQL column string.
188
- * @param column Column configuration object
189
- * @returns Complete column definition string (e.g., "id INTEGER NOT NULL")
190
- */
191
- function buildColumnDefinition(column) {
192
- const { name, references, schema, unique } = column;
193
- const { defaultValue, nullable, SQLiteType } = zodToSQLite(schema);
194
- const parts = [name, SQLiteType];
195
- if (!nullable) parts.push("NOT NULL");
196
- if (defaultValue !== void 0) parts.push(formatDefaultValue(defaultValue, SQLiteType));
197
- if (unique) parts.push("UNIQUE");
198
- const checkConstraint = formatCheckConstraint(name, schema);
199
- if (checkConstraint) parts.push(checkConstraint);
200
- if (references) {
201
- const { column: refColumn, onDelete, onUpdate, table } = references;
202
- parts.push(`REFERENCES ${table}(${refColumn})`);
203
- if (onDelete) parts.push(`ON DELETE ${onDelete}`);
204
- if (onUpdate) parts.push(`ON UPDATE ${onUpdate}`);
205
- }
206
- return parts.join(" ");
207
- }
208
- //#endregion
209
- //#region src/index-statements.ts
210
- /**
211
- * Generates SQL statements for table indexes.
212
- *
213
- * Creates standard, unique, and partial indexes based on configuration.
214
- * @param tableName Name of the table
215
- * @param indexes Array of index configurations
216
- * @returns Array of CREATE INDEX SQL statements
217
- */
218
- function buildIndexStatements(tableName, indexes) {
219
- if (!indexes || indexes.length === 0) return [];
220
- return indexes.map((index) => {
221
- const uniqueClause = index.unique ? "UNIQUE " : "";
222
- const whereClause = index.where ? ` WHERE ${index.where}` : "";
223
- return `CREATE ${uniqueClause}INDEX ${index.name} ON ${tableName} (${index.columns.join(", ")})${whereClause};`;
224
- });
225
- }
226
- //#endregion
227
- //#region src/primary-key-constraint.ts
228
- /**
229
- * Builds the PRIMARY KEY constraint string.
230
- *
231
- * Handles both single-column and composite primary keys.
232
- * @param primaryKeys Array of column names forming the primary key
233
- * @returns SQL constraint string (e.g., "PRIMARY KEY (id, email)")
234
- */
235
- function buildPrimaryKeyConstraint(primaryKeys) {
236
- if (primaryKeys.length === 0) return "";
237
- return `PRIMARY KEY (${primaryKeys.join(", ")})`;
238
- }
239
- //#endregion
240
- //#region src/zod-schema.ts
241
- /**
242
- * Builds a Zod schema from column definitions.
243
- *
244
- * Creates a Zod object schema where keys are column names and values are
245
- * the corresponding Zod schemas from the column configuration.
246
- * @template TColumns - Array of column configurations
247
- * @param columns Array of column configurations
248
- * @returns Zod object schema
249
- */
250
- function buildZodSchema(columns) {
251
- const schemaShape = {};
252
- for (const column of columns) schemaShape[column.name] = column.schema;
253
- return z.object(schemaShape);
254
- }
255
- //#endregion
256
- //#region src/table.ts
257
- /**
258
- * Creates a table definition and Zod schema.
259
- *
260
- * This function is the main entry point for defining a table. It takes a configuration object
261
- * and returns:
262
- * 1. `table`: SQL CREATE TABLE statement
263
- * 2. `indexes`: Array of SQL CREATE INDEX statements
264
- * 3. `schema`: Zod object schema matching the table structure
265
- * @template TColumns - Array of column configurations
266
- * @param config Table configuration object
267
- * @returns Object containing SQL statements and Zod schema
268
- */
269
- function createTable(config) {
270
- const { columns, indexes, name, primaryKeys } = config;
271
- const columnDefs = columns.map((column) => buildColumnDefinition(column));
272
- const primaryKeyConstraint = buildPrimaryKeyConstraint(primaryKeys);
273
- const createTableSQL = `CREATE TABLE ${name} (
274
- ${[...columnDefs, primaryKeyConstraint].filter(Boolean).join(",\n ")}
275
- );`;
276
- return {
277
- indexes: buildIndexStatements(name, indexes),
278
- schema: buildZodSchema(columns),
279
- table: createTableSQL
280
- };
281
- }
282
- //#endregion
283
- export { createTable };
1
+ import{z as e}from"zod";function t(e,t){let n=t;for(;;){let e=n._zod.def;if(e.type===`nullable`||e.type===`optional`||e.type==="default")n=e.innerType;else break}let r=n._zod.def;switch(r.type){case`enum`:{let t=r.entries;return`CHECK(${e} IN (${Object.values(t).map(e=>`'${String(e).replaceAll(`'`,`''`)}'`).join(`, `)}))`}case`literal`:{let t=r.values;if(t.length===1){let n=t[0];return typeof n==`string`?`CHECK(${e} = '${n.replaceAll(`'`,`''`)}')`:`CHECK(${e} = ${n})`}return`CHECK(${e} IN (${t.map(e=>typeof e==`string`?`'${e.replaceAll(`'`,`''`)}'`:String(e)).join(`, `)}))`}case`number`:{let t=r.checks;if(!t)return;let n=[],i=(t,r)=>{switch(t){case`greater_than`:n.push(`${e} >= ${r}`);break;case`less_than`:n.push(`${e} <= ${r}`)}};for(let e of t){let t=e._zod.def.check,n=e._zod.def.value;i(t,n)}return n.length>0?`CHECK(${n.join(` AND `)})`:void 0}case`string`:{let t=r.checks;if(!t)return;let n=[],i=(t,r)=>{switch(t){case`length_equals`:n.push(`length(${e}) = ${r.length}`);break;case`max_length`:n.push(`length(${e}) <= ${r.maximum}`);break;case`min_length`:n.push(`length(${e}) >= ${r.minimum}`)}};for(let e of t){let t=e._zod.def.check,n=e._zod.def;i(t,n)}return n.length>0?`CHECK(${n.join(` AND `)})`:void 0}case`union`:{let t=r.options;if(!t.every(e=>e._zod.def.type===`literal`))return;let n=[];for(let e of t)n.push(...e._zod.def.values);return`CHECK(${e} IN (${n.map(e=>typeof e==`string`?`'${e.replaceAll(`'`,`''`)}'`:String(e)).join(`, `)}))`}}}function n(e,t){switch(t){case`TEXT`:return`DEFAULT '${String(e).replaceAll(`'`,`''`)}'`;case`INTEGER`:case`REAL`:return`DEFAULT ${Number(e)}`;case`BOOLEAN`:return`DEFAULT ${+!!e}`;case`NULL`:return`DEFAULT NULL`;default:return`DEFAULT '${e}'`}}function r(e,t){switch(e){case`string`:switch(t._zod.def.format){case`date`:return`DATE`;case`datetime`:return`DATETIME`;case`duration`:return`INTEGER`;default:return`TEXT`}case`enum`:case`literal`:case`array`:case`template_literal`:case`object`:return`TEXT`;case`date`:return`DATE`;case`number`:switch(t._zod.def.format){case`safeint`:case`uint32`:case`int32`:return`INTEGER`;case`float32`:case`float64`:return`FLOAT`;default:return`REAL`}case`boolean`:return`BOOLEAN`;case`bigint`:return`BIGINT`;case`null`:case`undefined`:return`NULL`;case`file`:return`BLOB`;default:return`TEXT`}}function i(e){let t=!1,n,i=e;for(;;){let e=i._zod.def,r=e.type;if(r===`nullable`||r===`optional`)t=!0,i=e.innerType;else if(r==="default")n=typeof e.defaultValue==`function`?e.defaultValue():e.defaultValue,i=e.innerType;else break}let a=i._zod.def.type,o=r(a,i);return{defaultValue:n,nullable:t,schema:e,SQLiteType:o}}function a(e){let{name:r,references:a,schema:o,unique:s}=e,{defaultValue:c,nullable:l,SQLiteType:u}=i(o),d=[r,u];l||d.push(`NOT NULL`),c!==void 0&&d.push(n(c,u)),s&&d.push(`UNIQUE`);let f=t(r,o);if(f&&d.push(f),a){let{column:e,onDelete:t,onUpdate:n,table:r}=a;d.push(`REFERENCES ${r}(${e})`),t&&d.push(`ON DELETE ${t}`),n&&d.push(`ON UPDATE ${n}`)}return d.join(` `)}function o(e,t){return!t||t.length===0?[]:t.map(t=>{let n=t.unique?`UNIQUE `:``,r=t.where?` WHERE ${t.where}`:``;return`CREATE ${n}INDEX ${t.name} ON ${e} (${t.columns.join(`, `)})${r};`})}function s(e){return e.length===0?``:`PRIMARY KEY (${e.join(`, `)})`}function c(t){let n={};for(let e of t)n[e.name]=e.schema;return e.object(n)}function l(e){let{columns:t,indexes:n,name:r,primaryKeys:i}=e,l=t.map(e=>a(e)),u=s(i),d=`CREATE TABLE ${r} (
2
+ ${[...l,u].filter(Boolean).join(`,
3
+ `)}
4
+ );`;return{indexes:o(r,n),schema:c(t),table:d}}export{l as createTable};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zod-sqlite",
3
3
  "type": "module",
4
- "version": "1.0.4",
4
+ "version": "1.0.5",
5
5
  "private": false,
6
6
  "description": "Generate type-safe SQLite table schemas from Zod validation schemas.",
7
7
  "author": "Favour Emeka <favorodera@gmail.com>",
@@ -35,23 +35,22 @@
35
35
  "access": "public"
36
36
  },
37
37
  "engines": {
38
- "node": ">=22.0.0",
39
- "pnpm": ">=11.0.0"
38
+ "node": ">=24.0.0"
40
39
  },
41
40
  "dependencies": {
42
41
  "zod": "^4.4.3"
43
42
  },
44
43
  "devDependencies": {
45
- "@favorodera/eslint-config": "^1.0.4",
44
+ "@favorodera/eslint-config": "^3.0.0",
46
45
  "@types/node": "^26.2.0",
47
- "@typescript/native-preview": "7.0.0-dev.20260707.2",
48
- "@vitest/ui": "4.1.10",
49
- "db0": "^0.3.4",
46
+ "@vitest/ui": "5.0.5",
47
+ "db0": "^0.4.0",
50
48
  "eslint": "^10.8.1",
49
+ "pkg-pr-new": "^0.0.93",
51
50
  "relizy": "^1.4.9",
52
- "tsdown": "^0.22.14",
51
+ "tsdown": "^0.23.0",
53
52
  "typescript": "^6.0.3",
54
- "vitest": "^4.1.10"
53
+ "vitest": "^5.0.0"
55
54
  },
56
55
  "scripts": {
57
56
  "build": "tsdown",
@@ -59,7 +58,8 @@
59
58
  "test": "vitest run",
60
59
  "test:watch": "vitest --watch --ui",
61
60
  "typecheck": "tsc --noEmit",
62
- "lint": "eslint . --fix",
61
+ "lint:fix": "eslint . --fix",
62
+ "lint": "eslint .",
63
63
  "ready": "pnpm install && pnpm lint && pnpm typecheck && pnpm test && pnpm build"
64
64
  }
65
65
  }