@firtoz/drizzle-utils 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +270 -0
- package/package.json +67 -0
- package/src/collection-utils.ts +76 -0
- package/src/index.ts +38 -0
- package/src/syncableTable.ts +108 -0
- package/src/types.ts +108 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @firtoz/drizzle-utils
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#22](https://github.com/firtoz/fullstack-toolkit/pull/22) [`05e88e7`](https://github.com/firtoz/fullstack-toolkit/commit/05e88e775f262488d1da2b579eadd560cee2eba9) Thanks [@firtoz](https://github.com/firtoz)! - Initial release of `@firtoz/drizzle-utils` - Shared utilities and types for Drizzle ORM-based packages.
|
|
8
|
+
|
|
9
|
+
> **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Syncable Table Builder
|
|
14
|
+
|
|
15
|
+
- **`syncableTable`** - Creates SQLite tables with automatic timestamp tracking
|
|
16
|
+
- Auto-generates UUID primary keys with type branding
|
|
17
|
+
- Includes `id`, `createdAt`, `updatedAt`, and `deletedAt` columns
|
|
18
|
+
- Validates that default values are compatible with IndexedDB (no SQL expressions)
|
|
19
|
+
- Full TypeScript type safety with branded IDs
|
|
20
|
+
|
|
21
|
+
### Column Helpers
|
|
22
|
+
|
|
23
|
+
- **`idColumn`** - Branded text primary key column
|
|
24
|
+
- **`createdAtColumn`** - Integer timestamp with automatic default (now)
|
|
25
|
+
- **`updatedAtColumn`** - Integer timestamp with automatic default (now)
|
|
26
|
+
- **`deletedAtColumn`** - Nullable integer timestamp for soft deletes
|
|
27
|
+
|
|
28
|
+
### Type Utilities
|
|
29
|
+
|
|
30
|
+
- **Branded IDs** - Type-safe string IDs with table-specific branding
|
|
31
|
+
- `TableId<TTableName>` - Table-specific branded ID type
|
|
32
|
+
- `IdOf<TTable>` - Extract ID type from a table
|
|
33
|
+
- `makeId()` - Safely create branded IDs
|
|
34
|
+
- **Schema Helpers** - Type-safe Valibot schema inference
|
|
35
|
+
- `SelectSchema<TTable>` - Infer select schema from table
|
|
36
|
+
- `InsertSchema<TTable>` - Infer insert schema from table
|
|
37
|
+
|
|
38
|
+
### Migration Types
|
|
39
|
+
|
|
40
|
+
Shared TypeScript types for Drizzle migrations across different database backends:
|
|
41
|
+
|
|
42
|
+
- **Journal Types** - `Journal`, `JournalEntry`
|
|
43
|
+
- **Schema Definition Types** - `TableDefinition`, `ColumnDefinition`, `IndexDefinition`, `ForeignKeyDefinition`, `ViewDefinition`, `EnumDefinition`, etc.
|
|
44
|
+
- **Snapshot Types** - `Snapshot`, `SnapshotMeta`, `SnapshotInternal`
|
|
45
|
+
|
|
46
|
+
These types enable consistent migration handling in both IndexedDB and SQLite WASM packages.
|
|
47
|
+
|
|
48
|
+
## Example
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { syncableTable, idColumn } from "@firtoz/drizzle-utils";
|
|
52
|
+
import { text } from "drizzle-orm/sqlite-core";
|
|
53
|
+
|
|
54
|
+
// Create a table with automatic timestamp tracking
|
|
55
|
+
const todoTable = syncableTable("todos", {
|
|
56
|
+
title: text("title").notNull(),
|
|
57
|
+
completed: integer("completed", { mode: "boolean" })
|
|
58
|
+
.notNull()
|
|
59
|
+
.default(false),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// The table automatically includes:
|
|
63
|
+
// - id: TableId<"todos"> (UUID primary key)
|
|
64
|
+
// - createdAt: Date (auto-set on insert)
|
|
65
|
+
// - updatedAt: Date (auto-set on insert/update)
|
|
66
|
+
// - deletedAt: Date | null (for soft deletes)
|
|
67
|
+
|
|
68
|
+
type Todo = typeof todoTable.$inferSelect;
|
|
69
|
+
// {
|
|
70
|
+
// id: TableId<"todos">;
|
|
71
|
+
// title: string;
|
|
72
|
+
// completed: boolean;
|
|
73
|
+
// createdAt: Date;
|
|
74
|
+
// updatedAt: Date;
|
|
75
|
+
// deletedAt: Date | null;
|
|
76
|
+
// }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Dependencies
|
|
80
|
+
|
|
81
|
+
- `drizzle-orm` (peer dependency)
|
|
82
|
+
- `drizzle-valibot` (peer dependency)
|
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# @firtoz/drizzle-utils
|
|
2
|
+
|
|
3
|
+
Shared utilities and types for Drizzle ORM-based packages. Provides type-safe table builders with automatic timestamp tracking, branded IDs, and common migration types.
|
|
4
|
+
|
|
5
|
+
> **⚠️ Early WIP Notice:** This package is in very early development and is **not production-ready**. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @firtoz/drizzle-utils drizzle-orm drizzle-valibot
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
### 🏗️ Syncable Table Builder
|
|
16
|
+
|
|
17
|
+
Create SQLite tables with automatic timestamp tracking and UUID primary keys:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { syncableTable } from "@firtoz/drizzle-utils";
|
|
21
|
+
import { text, integer } from "drizzle-orm/sqlite-core";
|
|
22
|
+
|
|
23
|
+
const todoTable = syncableTable("todos", {
|
|
24
|
+
title: text("title").notNull(),
|
|
25
|
+
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
|
|
26
|
+
description: text("description"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Automatically includes:
|
|
30
|
+
// - id: TableId<"todos"> (UUID primary key)
|
|
31
|
+
// - createdAt: Date (auto-set on insert)
|
|
32
|
+
// - updatedAt: Date (auto-set on update)
|
|
33
|
+
// - deletedAt: Date | null (for soft deletes)
|
|
34
|
+
|
|
35
|
+
type Todo = typeof todoTable.$inferSelect;
|
|
36
|
+
// {
|
|
37
|
+
// id: TableId<"todos">;
|
|
38
|
+
// title: string;
|
|
39
|
+
// completed: boolean;
|
|
40
|
+
// description: string | null;
|
|
41
|
+
// createdAt: Date;
|
|
42
|
+
// updatedAt: Date;
|
|
43
|
+
// deletedAt: Date | null;
|
|
44
|
+
// }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 🏷️ Branded ID Types
|
|
48
|
+
|
|
49
|
+
Type-safe IDs with table-specific branding prevent mixing IDs from different tables:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { makeId, type IdOf } from "@firtoz/drizzle-utils";
|
|
53
|
+
|
|
54
|
+
const todoId = makeId(todoTable, "123e4567-e89b-12d3-a456-426614174000");
|
|
55
|
+
const userId = makeId(userTable, "123e4567-e89b-12d3-a456-426614174000");
|
|
56
|
+
|
|
57
|
+
// TypeScript prevents mixing different table IDs
|
|
58
|
+
function getTodo(id: IdOf<typeof todoTable>) { /* ... */ }
|
|
59
|
+
|
|
60
|
+
getTodo(todoId); // ✅ OK
|
|
61
|
+
getTodo(userId); // ❌ Type error - wrong table!
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 📋 Column Helpers
|
|
65
|
+
|
|
66
|
+
Individual column builders for custom table definitions:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
idColumn,
|
|
71
|
+
createdAtColumn,
|
|
72
|
+
updatedAtColumn,
|
|
73
|
+
deletedAtColumn
|
|
74
|
+
} from "@firtoz/drizzle-utils";
|
|
75
|
+
|
|
76
|
+
// Use in custom table definitions
|
|
77
|
+
const customTable = sqliteTable("custom", {
|
|
78
|
+
id: idColumn,
|
|
79
|
+
name: text("name").notNull(),
|
|
80
|
+
createdAt: createdAtColumn,
|
|
81
|
+
updatedAt: updatedAtColumn,
|
|
82
|
+
deletedAt: deletedAtColumn,
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 🔄 Migration Types
|
|
87
|
+
|
|
88
|
+
Shared TypeScript types for Drizzle migrations across IndexedDB and SQLite:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import type {
|
|
92
|
+
Journal,
|
|
93
|
+
JournalEntry,
|
|
94
|
+
Snapshot,
|
|
95
|
+
TableDefinition,
|
|
96
|
+
ColumnDefinition,
|
|
97
|
+
IndexDefinition
|
|
98
|
+
} from "@firtoz/drizzle-utils";
|
|
99
|
+
|
|
100
|
+
// Use these types for custom migration logic
|
|
101
|
+
function applyMigration(snapshot: Snapshot) {
|
|
102
|
+
for (const [tableName, table] of Object.entries(snapshot.tables)) {
|
|
103
|
+
const tableDef: TableDefinition = table;
|
|
104
|
+
// ... migration logic
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 📝 Schema Type Helpers
|
|
110
|
+
|
|
111
|
+
Type-safe Valibot schema inference:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { type SelectSchema, type InsertSchema } from "@firtoz/drizzle-utils";
|
|
115
|
+
|
|
116
|
+
type TodoSelect = SelectSchema<typeof todoTable>;
|
|
117
|
+
type TodoInsert = InsertSchema<typeof todoTable>;
|
|
118
|
+
|
|
119
|
+
// Use with Valibot for runtime validation
|
|
120
|
+
import { parse } from "valibot";
|
|
121
|
+
import { createSelectSchema, createInsertSchema } from "drizzle-valibot";
|
|
122
|
+
|
|
123
|
+
const selectSchema = createSelectSchema(todoTable);
|
|
124
|
+
const insertSchema = createInsertSchema(todoTable);
|
|
125
|
+
|
|
126
|
+
const validTodo = parse(selectSchema, data);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API Reference
|
|
130
|
+
|
|
131
|
+
### Table Builders
|
|
132
|
+
|
|
133
|
+
#### `syncableTable(name, columns, extraConfig?)`
|
|
134
|
+
|
|
135
|
+
Creates a SQLite table with automatic timestamp tracking.
|
|
136
|
+
|
|
137
|
+
**Parameters:**
|
|
138
|
+
- `name: string` - Table name
|
|
139
|
+
- `columns: Record<string, SQLiteColumnBuilder>` - Column definitions (cannot use `id`, `createdAt`, `updatedAt`, or `deletedAt` as keys)
|
|
140
|
+
- `extraConfig?: (self) => SQLiteTableExtraConfigValue[]` - Optional indexes and constraints
|
|
141
|
+
|
|
142
|
+
**Returns:** SQLite table with id, createdAt, updatedAt, deletedAt columns
|
|
143
|
+
|
|
144
|
+
**Validation:**
|
|
145
|
+
- Ensures default values are compatible with IndexedDB (no SQL expressions)
|
|
146
|
+
- Throws error if SQL expressions are used as default values
|
|
147
|
+
|
|
148
|
+
### Column Helpers
|
|
149
|
+
|
|
150
|
+
#### `idColumn`
|
|
151
|
+
|
|
152
|
+
Text column configured as primary key with branded UUID type.
|
|
153
|
+
|
|
154
|
+
#### `createdAtColumn`
|
|
155
|
+
|
|
156
|
+
Integer timestamp column (mode: "timestamp") with automatic default (current date).
|
|
157
|
+
|
|
158
|
+
#### `updatedAtColumn`
|
|
159
|
+
|
|
160
|
+
Integer timestamp column (mode: "timestamp") with automatic default (current date).
|
|
161
|
+
|
|
162
|
+
#### `deletedAtColumn`
|
|
163
|
+
|
|
164
|
+
Nullable integer timestamp column (mode: "timestamp") for soft deletes.
|
|
165
|
+
|
|
166
|
+
### Type Utilities
|
|
167
|
+
|
|
168
|
+
#### `Branded<T, Brand>`
|
|
169
|
+
|
|
170
|
+
Creates a branded type for better type safety.
|
|
171
|
+
|
|
172
|
+
#### `TableId<TTableName>`
|
|
173
|
+
|
|
174
|
+
Table-specific branded ID type with table name in the brand.
|
|
175
|
+
|
|
176
|
+
#### `IdOf<TTable>`
|
|
177
|
+
|
|
178
|
+
Extracts the ID type from a Drizzle table.
|
|
179
|
+
|
|
180
|
+
#### `makeId<TTable>(table, value)`
|
|
181
|
+
|
|
182
|
+
Safely creates a branded ID for a specific table.
|
|
183
|
+
|
|
184
|
+
#### `SelectSchema<TTable>`
|
|
185
|
+
|
|
186
|
+
Infers the Valibot select schema type from a Drizzle table.
|
|
187
|
+
|
|
188
|
+
#### `InsertSchema<TTable>`
|
|
189
|
+
|
|
190
|
+
Infers the Valibot insert schema type from a Drizzle table.
|
|
191
|
+
|
|
192
|
+
### Migration Types
|
|
193
|
+
|
|
194
|
+
Comprehensive types for database migrations:
|
|
195
|
+
|
|
196
|
+
- `Journal` - Migration journal with version and entries
|
|
197
|
+
- `JournalEntry` - Individual migration record
|
|
198
|
+
- `Snapshot` - Complete database schema snapshot
|
|
199
|
+
- `TableDefinition` - Table structure definition
|
|
200
|
+
- `ColumnDefinition` - Column configuration
|
|
201
|
+
- `IndexDefinition` - Index configuration
|
|
202
|
+
- `ForeignKeyDefinition` - Foreign key constraint
|
|
203
|
+
- `ViewDefinition` - Database view definition
|
|
204
|
+
- `EnumDefinition` - Enum type definition
|
|
205
|
+
|
|
206
|
+
## Best Practices
|
|
207
|
+
|
|
208
|
+
### 1. Use syncableTable for Data Tables
|
|
209
|
+
|
|
210
|
+
Always use `syncableTable` for tables that need timestamp tracking:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// ✅ Good - automatic timestamps
|
|
214
|
+
const todoTable = syncableTable("todos", {
|
|
215
|
+
title: text("title").notNull(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ❌ Bad - manual timestamp management
|
|
219
|
+
const todoTable = sqliteTable("todos", {
|
|
220
|
+
id: text("id").primaryKey(),
|
|
221
|
+
title: text("title").notNull(),
|
|
222
|
+
createdAt: integer("createdAt", { mode: "timestamp" }),
|
|
223
|
+
// ... repetitive boilerplate
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 2. Leverage Branded IDs
|
|
228
|
+
|
|
229
|
+
Use branded IDs to prevent mixing IDs from different tables:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
type TodoId = IdOf<typeof todoTable>;
|
|
233
|
+
type UserId = IdOf<typeof userTable>;
|
|
234
|
+
|
|
235
|
+
function assignTodo(todoId: TodoId, userId: UserId) {
|
|
236
|
+
// Type safety ensures correct ID types are used
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 3. Avoid SQL Expressions in Defaults
|
|
241
|
+
|
|
242
|
+
The `syncableTable` validates that default values work with IndexedDB:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// ✅ Good - JavaScript default
|
|
246
|
+
const table = syncableTable("table", {
|
|
247
|
+
status: text("status").default("pending"),
|
|
248
|
+
count: integer("count").default(0),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ❌ Bad - SQL expression (will throw error)
|
|
252
|
+
const table = syncableTable("table", {
|
|
253
|
+
status: text("status").default(sql`'pending'`), // Error!
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Integration
|
|
258
|
+
|
|
259
|
+
This package is used by:
|
|
260
|
+
- `@firtoz/drizzle-indexeddb` - IndexedDB migrations
|
|
261
|
+
- `@firtoz/drizzle-sqlite-wasm` - SQLite WASM integration
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT
|
|
266
|
+
|
|
267
|
+
## Author
|
|
268
|
+
|
|
269
|
+
Firtina Ozbalikchi <firtoz@github.com>
|
|
270
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firtoz/drizzle-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities and types for Drizzle-based packages",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"module": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"import": "./src/index.ts",
|
|
13
|
+
"require": "./src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"./*": {
|
|
16
|
+
"types": "./src/*.ts",
|
|
17
|
+
"import": "./src/*.ts",
|
|
18
|
+
"require": "./src/*.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src/**/*.ts",
|
|
23
|
+
"!src/**/*.test.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"typecheck": "tsc --noEmit -p ./tsconfig.json",
|
|
29
|
+
"lint": "biome check --write src",
|
|
30
|
+
"lint:ci": "biome ci src",
|
|
31
|
+
"format": "biome format src --write",
|
|
32
|
+
"test": "bun test --pass-with-no-tests",
|
|
33
|
+
"test:watch": "bun test --watch"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"typescript",
|
|
37
|
+
"drizzle",
|
|
38
|
+
"utils"
|
|
39
|
+
],
|
|
40
|
+
"author": "Firtina Ozbalikchi <firtoz@github.com>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/firtoz/fullstack-toolkit.git",
|
|
46
|
+
"directory": "packages/drizzle-utils"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/firtoz/fullstack-toolkit/issues"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@tanstack/db": "^0.5.0",
|
|
59
|
+
"drizzle-orm": "^0.44.7",
|
|
60
|
+
"drizzle-valibot": "^0.4.2"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@tanstack/db": "^0.5.0",
|
|
64
|
+
"drizzle-orm": "^0.44.7",
|
|
65
|
+
"drizzle-valibot": "^0.4.2"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Table } from "drizzle-orm";
|
|
2
|
+
import type { BuildSchema } from "drizzle-valibot";
|
|
3
|
+
import type { Collection, UtilsRecord } from "@tanstack/db";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utility type for branded IDs
|
|
7
|
+
*/
|
|
8
|
+
export type Branded<T, Brand> = T & { __brand: Brand };
|
|
9
|
+
|
|
10
|
+
export type TableId<TTableName extends string> = Branded<
|
|
11
|
+
string,
|
|
12
|
+
`${TTableName}_id`
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Utility type to extract the ID type from a table
|
|
17
|
+
*/
|
|
18
|
+
export type IdOf<TTable extends Table> = TTable extends {
|
|
19
|
+
$inferSelect: { id: infer TId extends string | number };
|
|
20
|
+
}
|
|
21
|
+
? TId
|
|
22
|
+
: string | number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Utility function to safely create branded IDs
|
|
26
|
+
*/
|
|
27
|
+
export function makeId<TTable extends Table>(
|
|
28
|
+
_table: TTable,
|
|
29
|
+
value: string,
|
|
30
|
+
): IdOf<TTable> {
|
|
31
|
+
return value as IdOf<TTable>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Select schema type helper
|
|
36
|
+
*/
|
|
37
|
+
export type SelectSchema<TTable extends Table> = BuildSchema<
|
|
38
|
+
"select",
|
|
39
|
+
TTable["_"]["columns"],
|
|
40
|
+
undefined
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Insert schema type helper
|
|
45
|
+
*/
|
|
46
|
+
export type InsertSchema<TTable extends Table> = BuildSchema<
|
|
47
|
+
"insert",
|
|
48
|
+
TTable["_"]["columns"],
|
|
49
|
+
undefined
|
|
50
|
+
>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper type to get the table from schema by name
|
|
54
|
+
*/
|
|
55
|
+
export type GetTableFromSchema<
|
|
56
|
+
TSchema extends Record<string, unknown>,
|
|
57
|
+
TTableName extends keyof TSchema,
|
|
58
|
+
> = TSchema[TTableName] extends Table ? TSchema[TTableName] : never;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Helper type to infer the collection type from table
|
|
62
|
+
* This provides proper typing for Collection insert/update operations
|
|
63
|
+
*/
|
|
64
|
+
export type InferCollectionFromTable<TTable extends Table> = Collection<
|
|
65
|
+
TTable["$inferSelect"],
|
|
66
|
+
IdOf<TTable>,
|
|
67
|
+
UtilsRecord,
|
|
68
|
+
SelectSchema<TTable>,
|
|
69
|
+
Omit<
|
|
70
|
+
TTable["$inferInsert"],
|
|
71
|
+
"id"
|
|
72
|
+
// "createdAt" | "updatedAt" | "deletedAt" | "id"
|
|
73
|
+
> & {
|
|
74
|
+
id?: IdOf<TTable>;
|
|
75
|
+
}
|
|
76
|
+
>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
JournalEntry,
|
|
3
|
+
Journal,
|
|
4
|
+
SqliteColumnType,
|
|
5
|
+
ColumnDefinition,
|
|
6
|
+
IndexDefinition,
|
|
7
|
+
ForeignKeyDefinition,
|
|
8
|
+
CompositePrimaryKeyDefinition,
|
|
9
|
+
UniqueConstraintDefinition,
|
|
10
|
+
CheckConstraintDefinition,
|
|
11
|
+
TableDefinition,
|
|
12
|
+
ViewDefinition,
|
|
13
|
+
EnumDefinition,
|
|
14
|
+
SnapshotMeta,
|
|
15
|
+
SnapshotInternal,
|
|
16
|
+
Snapshot,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
Branded,
|
|
21
|
+
TableId,
|
|
22
|
+
IdOf,
|
|
23
|
+
SelectSchema,
|
|
24
|
+
InsertSchema,
|
|
25
|
+
GetTableFromSchema,
|
|
26
|
+
InferCollectionFromTable,
|
|
27
|
+
} from "./collection-utils";
|
|
28
|
+
|
|
29
|
+
export { makeId } from "./collection-utils";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
createdAtColumn,
|
|
33
|
+
updatedAtColumn,
|
|
34
|
+
deletedAtColumn,
|
|
35
|
+
syncableTable,
|
|
36
|
+
} from "./syncableTable";
|
|
37
|
+
|
|
38
|
+
export type { TableWithRequiredFields } from "./syncableTable";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
integer,
|
|
3
|
+
text,
|
|
4
|
+
sqliteTable,
|
|
5
|
+
type SQLiteColumnBuilderBase,
|
|
6
|
+
type TableConfig,
|
|
7
|
+
type SQLiteTableExtraConfigValue,
|
|
8
|
+
type SQLiteTableWithColumns,
|
|
9
|
+
} from "drizzle-orm/sqlite-core";
|
|
10
|
+
import { type BuildColumns, getTableColumns, SQL } from "drizzle-orm";
|
|
11
|
+
import type { TableId } from "./collection-utils";
|
|
12
|
+
|
|
13
|
+
const createTableIdColumn = <TTableName extends string>() =>
|
|
14
|
+
text("id")
|
|
15
|
+
.primaryKey()
|
|
16
|
+
.$type<TableId<TTableName>>()
|
|
17
|
+
.$defaultFn(() => {
|
|
18
|
+
return crypto.randomUUID() as TableId<TTableName>;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Use unixepoch with 'subsec' modifier for millisecond precision timestamps
|
|
22
|
+
export const createdAtColumn = integer("createdAt", { mode: "timestamp_ms" })
|
|
23
|
+
.$defaultFn(() => new Date())
|
|
24
|
+
.notNull();
|
|
25
|
+
|
|
26
|
+
export const updatedAtColumn = integer("updatedAt", { mode: "timestamp_ms" })
|
|
27
|
+
.$defaultFn(() => new Date())
|
|
28
|
+
.notNull();
|
|
29
|
+
|
|
30
|
+
export const deletedAtColumn = integer("deletedAt", {
|
|
31
|
+
mode: "timestamp_ms",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const syncableTable = <
|
|
35
|
+
TTableName extends string,
|
|
36
|
+
TColumns extends Record<string, SQLiteColumnBuilderBase> & {
|
|
37
|
+
id?: never;
|
|
38
|
+
createdAt?: never;
|
|
39
|
+
updatedAt?: never;
|
|
40
|
+
deletedAt?: never;
|
|
41
|
+
},
|
|
42
|
+
>(
|
|
43
|
+
tableName: TTableName,
|
|
44
|
+
columns: TColumns,
|
|
45
|
+
extraConfig?: (
|
|
46
|
+
self: BuildColumns<
|
|
47
|
+
TTableName,
|
|
48
|
+
Omit<TColumns, "id" | "createdAt" | "updatedAt" | "deletedAt"> & {
|
|
49
|
+
id: ReturnType<typeof createTableIdColumn<TTableName>>;
|
|
50
|
+
createdAt: typeof createdAtColumn;
|
|
51
|
+
updatedAt: typeof updatedAtColumn;
|
|
52
|
+
deletedAt: typeof deletedAtColumn;
|
|
53
|
+
},
|
|
54
|
+
"sqlite"
|
|
55
|
+
>,
|
|
56
|
+
) => SQLiteTableExtraConfigValue[],
|
|
57
|
+
) => {
|
|
58
|
+
const tableIdColumn = createTableIdColumn<TTableName>();
|
|
59
|
+
const table = sqliteTable(
|
|
60
|
+
tableName,
|
|
61
|
+
{
|
|
62
|
+
id: tableIdColumn,
|
|
63
|
+
createdAt: createdAtColumn,
|
|
64
|
+
updatedAt: updatedAtColumn,
|
|
65
|
+
deletedAt: deletedAtColumn,
|
|
66
|
+
...columns,
|
|
67
|
+
},
|
|
68
|
+
extraConfig,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const tableColumns = getTableColumns(table);
|
|
72
|
+
|
|
73
|
+
// console.log("table:", table);
|
|
74
|
+
|
|
75
|
+
for (const columnName in tableColumns) {
|
|
76
|
+
const column = tableColumns[columnName];
|
|
77
|
+
|
|
78
|
+
let defaultValue: unknown | undefined;
|
|
79
|
+
if (column.defaultFn) {
|
|
80
|
+
defaultValue = column.defaultFn();
|
|
81
|
+
} else if (column.default !== undefined) {
|
|
82
|
+
defaultValue = column.default;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (defaultValue instanceof SQL) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Default value for column ${tableName}.${columnName} is a SQL expression, which is not supported for IndexedDB.\n\nYou can use a default value or a default function instead.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return table;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type TableWithRequiredFields = SQLiteTableWithColumns<
|
|
96
|
+
Pick<TableConfig, "name" | "schema" | "dialect"> & {
|
|
97
|
+
columns: BuildColumns<
|
|
98
|
+
string,
|
|
99
|
+
{
|
|
100
|
+
id: ReturnType<typeof createTableIdColumn<string>>;
|
|
101
|
+
createdAt: typeof createdAtColumn;
|
|
102
|
+
updatedAt: typeof updatedAtColumn;
|
|
103
|
+
deletedAt: typeof deletedAtColumn;
|
|
104
|
+
},
|
|
105
|
+
"sqlite"
|
|
106
|
+
>;
|
|
107
|
+
}
|
|
108
|
+
>;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Shared types for Drizzle migrations across different database backends
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Journal Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export interface JournalEntry {
|
|
8
|
+
idx: number;
|
|
9
|
+
version: string;
|
|
10
|
+
when: number;
|
|
11
|
+
tag: string;
|
|
12
|
+
breakpoints: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Journal {
|
|
16
|
+
version: string;
|
|
17
|
+
dialect: string;
|
|
18
|
+
entries: JournalEntry[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Schema Definition Types
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
export type SqliteColumnType = "text" | "integer" | "real" | "blob" | "numeric";
|
|
26
|
+
|
|
27
|
+
export interface ColumnDefinition {
|
|
28
|
+
name: string;
|
|
29
|
+
type: SqliteColumnType | string;
|
|
30
|
+
primaryKey: boolean;
|
|
31
|
+
notNull: boolean;
|
|
32
|
+
autoincrement: boolean;
|
|
33
|
+
default?: string | number | boolean | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IndexDefinition {
|
|
37
|
+
name: string;
|
|
38
|
+
columns: string[];
|
|
39
|
+
isUnique: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ForeignKeyDefinition {
|
|
43
|
+
name: string;
|
|
44
|
+
tableFrom: string;
|
|
45
|
+
tableTo: string;
|
|
46
|
+
columnsFrom: string[];
|
|
47
|
+
columnsTo: string[];
|
|
48
|
+
onDelete?: "cascade" | "set null" | "set default" | "restrict" | "no action";
|
|
49
|
+
onUpdate?: "cascade" | "set null" | "set default" | "restrict" | "no action";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CompositePrimaryKeyDefinition {
|
|
53
|
+
name: string;
|
|
54
|
+
columns: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UniqueConstraintDefinition {
|
|
58
|
+
name: string;
|
|
59
|
+
columns: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CheckConstraintDefinition {
|
|
63
|
+
name: string;
|
|
64
|
+
value: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TableDefinition {
|
|
68
|
+
name: string;
|
|
69
|
+
columns: Record<string, ColumnDefinition>;
|
|
70
|
+
indexes: Record<string, IndexDefinition>;
|
|
71
|
+
foreignKeys: Record<string, ForeignKeyDefinition>;
|
|
72
|
+
compositePrimaryKeys: Record<string, CompositePrimaryKeyDefinition>;
|
|
73
|
+
uniqueConstraints: Record<string, UniqueConstraintDefinition>;
|
|
74
|
+
checkConstraints: Record<string, CheckConstraintDefinition>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ViewDefinition {
|
|
78
|
+
name: string;
|
|
79
|
+
query: string;
|
|
80
|
+
columns: Record<string, { name: string; type: string }>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface EnumDefinition {
|
|
84
|
+
name: string;
|
|
85
|
+
values: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SnapshotMeta {
|
|
89
|
+
schemas: Record<string, unknown>;
|
|
90
|
+
tables: Record<string, unknown>;
|
|
91
|
+
columns: Record<string, string>; // Old column name -> new column name
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SnapshotInternal {
|
|
95
|
+
indexes: Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface Snapshot {
|
|
99
|
+
version: string;
|
|
100
|
+
dialect: string;
|
|
101
|
+
id: string;
|
|
102
|
+
prevId: string;
|
|
103
|
+
tables: Record<string, TableDefinition>;
|
|
104
|
+
views: Record<string, ViewDefinition>;
|
|
105
|
+
enums: Record<string, EnumDefinition>;
|
|
106
|
+
_meta: SnapshotMeta;
|
|
107
|
+
internal: SnapshotInternal;
|
|
108
|
+
}
|