@sigitex/outlaw 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +333 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,9 +1,338 @@
|
|
|
1
1
|
# Outlaw
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A trigger-happy SQLite framework. Type-safe, schema-first, with automatic migrations.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```
|
|
6
|
+
bun add @sigitex/outlaw
|
|
7
|
+
```
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
Attempts to mirror Sqlite syntax *very* closely. Currently works with Bun and Cloudflare Functions.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Define your schema in code, and Outlaw's [cowboy migrations](#cowboy-migrations) automatically diff and converge your database on startup -- no migration files, no CLI steps. Just change your schema and go.
|
|
12
|
+
|
|
13
|
+
> **Note:** This package exports TypeScript sources directly. A TypeScript-compatible runtime or bundler (Bun, etc.) is required.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// 1. Define your schema
|
|
19
|
+
const users = createTable("users", {
|
|
20
|
+
id: integer.primaryKey.autoincrement,
|
|
21
|
+
name: text.notNull,
|
|
22
|
+
email: text.notNull.unique,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const schema = createSchema({ users })
|
|
26
|
+
|
|
27
|
+
// 2. Create a connection with auto-migration
|
|
28
|
+
const bun = new BunConnection(new Database("app.db"))
|
|
29
|
+
const connection = new CowboyConnection(bun, schema)
|
|
30
|
+
|
|
31
|
+
// 3. Use the typed database API
|
|
32
|
+
const db = createDatabase(connection, schema)
|
|
33
|
+
|
|
34
|
+
await db.users.insert({ name: "Wyatt", email: "wyatt@earp.com" }).execute()
|
|
35
|
+
|
|
36
|
+
const allUsers = await db.users.select("*").fetch()
|
|
37
|
+
const user = await db.users.select("*").where("id", 1).first()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Schema Definition
|
|
41
|
+
|
|
42
|
+
### Tables
|
|
43
|
+
|
|
44
|
+
Define tables with `createTable`. Each column uses a builder chain starting from a base type.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createTable, text, integer, real, blob } from "@sigitex/outlaw"
|
|
48
|
+
|
|
49
|
+
const products = createTable("products", {
|
|
50
|
+
id: integer.primaryKey.autoincrement,
|
|
51
|
+
name: text.notNull,
|
|
52
|
+
description: text, // nullable by default
|
|
53
|
+
price: real.notNull,
|
|
54
|
+
image: blob,
|
|
55
|
+
sku: text.notNull.unique,
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Column Types
|
|
60
|
+
|
|
61
|
+
| Builder | SQLite Type | TypeScript Type |
|
|
62
|
+
|-----------|-------------|-----------------|
|
|
63
|
+
| `text` | TEXT | `string` |
|
|
64
|
+
| `integer` | INTEGER | `number` |
|
|
65
|
+
| `real` | REAL | `number` |
|
|
66
|
+
| `blob` | BLOB | `ArrayBuffer` |
|
|
67
|
+
|
|
68
|
+
#### Column Modifiers
|
|
69
|
+
|
|
70
|
+
Modifiers are chained as properties or method calls:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
text.notNull // NOT NULL
|
|
74
|
+
integer.primaryKey // PRIMARY KEY
|
|
75
|
+
integer.primaryKey.autoincrement // PRIMARY KEY AUTOINCREMENT
|
|
76
|
+
text.unique // UNIQUE
|
|
77
|
+
text.default("'unknown'") // DEFAULT 'unknown'
|
|
78
|
+
text.check("length(name) > 0") // CHECK constraint
|
|
79
|
+
integer.foreignKey.references(other.id) // FOREIGN KEY
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Each modifier can only be used once per column -- the type system removes it after use.
|
|
83
|
+
|
|
84
|
+
#### Type Mappings
|
|
85
|
+
|
|
86
|
+
Map SQLite storage types to richer TypeScript types:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const events = createTable("events", {
|
|
90
|
+
id: integer.primaryKey.autoincrement,
|
|
91
|
+
active: integer.notNull.map.boolean, // stored as 0/1, typed as boolean
|
|
92
|
+
createdAt: integer.notNull.map.timestamp, // stored as epoch ms, typed as Date
|
|
93
|
+
scheduledFor: text.map.date, // stored as ISO string, typed as Date
|
|
94
|
+
metadata: text.map.json<{ tags: string[] }>(), // stored as JSON string, typed as object
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Table Constraints
|
|
99
|
+
|
|
100
|
+
Add composite constraints after column definitions:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const memberships = createTable("memberships", {
|
|
104
|
+
userId: integer.notNull.foreignKey.references(users.id),
|
|
105
|
+
groupId: integer.notNull.foreignKey.references(groups.id),
|
|
106
|
+
})
|
|
107
|
+
.primaryKey("userId", "groupId")
|
|
108
|
+
.unique("userId", "groupId")
|
|
109
|
+
.check("userId != groupId")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Views
|
|
113
|
+
|
|
114
|
+
Define views from query builders on existing tables:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { createView } from "@sigitex/outlaw"
|
|
118
|
+
|
|
119
|
+
const activeUsers = createView("active_users",
|
|
120
|
+
users.select("id", "name").where("active", 1)
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Indexes
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { createIndex, createUniqueIndex } from "@sigitex/outlaw"
|
|
128
|
+
|
|
129
|
+
const emailIndex = createUniqueIndex("idx_users_email").on(users.email)
|
|
130
|
+
const nameIndex = createIndex("idx_users_name").on(users.name)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Schema
|
|
134
|
+
|
|
135
|
+
Group tables, views, and indexes into a schema:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createSchema } from "@sigitex/outlaw"
|
|
139
|
+
|
|
140
|
+
const schema = createSchema({
|
|
141
|
+
users,
|
|
142
|
+
products,
|
|
143
|
+
memberships,
|
|
144
|
+
activeUsers,
|
|
145
|
+
emailIndex,
|
|
146
|
+
nameIndex,
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Connections
|
|
151
|
+
|
|
152
|
+
Outlaw abstracts over any SQLite connection via the `Connection` interface:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
type Connection = {
|
|
156
|
+
query<Row>(sql: string): Promise<Row[]>
|
|
157
|
+
script(statements: string[]): Promise<void>
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Two built-in adapters are provided:
|
|
162
|
+
|
|
163
|
+
### Bun
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { BunConnection } from "@sigitex/outlaw/bun"
|
|
167
|
+
import { Database } from "bun:sqlite"
|
|
168
|
+
|
|
169
|
+
const connection = new BunConnection(new Database("app.db"))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Cloudflare D1
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { CloudflareConnection } from "@sigitex/outlaw/cloudflare"
|
|
176
|
+
|
|
177
|
+
// Inside a Cloudflare Worker
|
|
178
|
+
const connection = new CloudflareConnection(env.DB)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Database API
|
|
182
|
+
|
|
183
|
+
`createDatabase` returns a typed object with an accessor for each table and view in the schema.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { createDatabase } from "@sigitex/outlaw"
|
|
187
|
+
|
|
188
|
+
const db = createDatabase(connection, schema)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Select
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// Select all columns
|
|
195
|
+
const rows = await db.users.select("*").fetch()
|
|
196
|
+
|
|
197
|
+
// Select specific columns
|
|
198
|
+
const names = await db.users.select("name", "email").fetch()
|
|
199
|
+
|
|
200
|
+
// Single result (throws if no match)
|
|
201
|
+
const user = await db.users.select("*").where("id", 1).first()
|
|
202
|
+
|
|
203
|
+
// Filtering
|
|
204
|
+
db.users.select("*")
|
|
205
|
+
.where("name", "Wyatt") // equality
|
|
206
|
+
.where("age", ">=", 21) // comparison operators
|
|
207
|
+
.where("deletedAt", "is null") // unary operators
|
|
208
|
+
|
|
209
|
+
// Sorting, pagination
|
|
210
|
+
db.users.select("*")
|
|
211
|
+
.orderBy([["name", "asc"], ["id", "desc"]])
|
|
212
|
+
.limit(10)
|
|
213
|
+
.offset(20)
|
|
214
|
+
.fetch()
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Joins
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
db.users.select("*")
|
|
221
|
+
.join(posts).on(users.id, "=", posts.userId)
|
|
222
|
+
.fetch()
|
|
223
|
+
|
|
224
|
+
db.users.select("*")
|
|
225
|
+
.leftJoin(posts).on(users.id, "=", posts.userId)
|
|
226
|
+
.fetch()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Join types: `join`, `leftJoin`, `rightJoin`, `crossJoin`. Each accepts a table or a subquery.
|
|
230
|
+
|
|
231
|
+
### Insert
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// Single row
|
|
235
|
+
await db.users.insert({ name: "Doc", email: "doc@ok.com" }).execute()
|
|
236
|
+
|
|
237
|
+
// With returning
|
|
238
|
+
const [inserted] = await db.users
|
|
239
|
+
.insert({ name: "Doc", email: "doc@ok.com" })
|
|
240
|
+
.returning("*")
|
|
241
|
+
.execute()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Update
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
await db.users
|
|
248
|
+
.update({ name: "Morgan" })
|
|
249
|
+
.where("id", 3)
|
|
250
|
+
.execute()
|
|
251
|
+
|
|
252
|
+
// With returning
|
|
253
|
+
const updated = await db.users
|
|
254
|
+
.update({ name: "Morgan" })
|
|
255
|
+
.where("id", 3)
|
|
256
|
+
.returning("*")
|
|
257
|
+
.execute()
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Delete
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
await db.users
|
|
264
|
+
.delete()
|
|
265
|
+
.where("id", 3)
|
|
266
|
+
.execute()
|
|
267
|
+
|
|
268
|
+
// With returning
|
|
269
|
+
const deleted = await db.users
|
|
270
|
+
.delete()
|
|
271
|
+
.where("id", 3)
|
|
272
|
+
.returning("*")
|
|
273
|
+
.execute()
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Cowboy Migrations
|
|
277
|
+
|
|
278
|
+
Wrap any connection with `CowboyConnection` to enable automatic schema migration. On the first query, Outlaw diffs the database against your schema and applies changes -- creating missing tables, rebuilding tables whose columns have changed, and managing views and indexes.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { CowboyConnection } from "@sigitex/outlaw"
|
|
282
|
+
|
|
283
|
+
const connection = new CowboyConnection(rawConnection, schema)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Schema metadata is stored in a `cowboy_migration` table. When columns change, Outlaw uses an interim table pattern: create the new table, copy data, drop the old one, rename.
|
|
287
|
+
|
|
288
|
+
### Schema Hacks
|
|
289
|
+
|
|
290
|
+
Destructive changes (renaming or dropping tables/columns) require explicit hints via `createSchemaHacker`, so data isn't silently lost:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { createSchemaHacker } from "@sigitex/outlaw"
|
|
294
|
+
|
|
295
|
+
const hack = createSchemaHacker()
|
|
296
|
+
|
|
297
|
+
hack.renamed.table("old_users", "users")
|
|
298
|
+
hack.renamed.column("users", "firstName", "name")
|
|
299
|
+
hack.dropped.table("legacy_data")
|
|
300
|
+
hack.dropped.column("users", "deprecated_field")
|
|
301
|
+
|
|
302
|
+
const connection = new CowboyConnection(rawConnection, schema, {
|
|
303
|
+
hacks: hack.hacks,
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Fixtures and Seeds
|
|
308
|
+
|
|
309
|
+
Pre-populate tables with `createFixture` (or its alias `createSeed`). Fixtures can use templates to provide default values and `RefBy` to reference rows in other tables.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import { createFixture } from "@sigitex/outlaw"
|
|
313
|
+
|
|
314
|
+
// Simple fixture
|
|
315
|
+
const userFixture = createFixture(users, [
|
|
316
|
+
{ name: "Wyatt", email: "wyatt@earp.com" },
|
|
317
|
+
{ name: "Doc", email: "doc@ok.com" },
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
// Fixture with a template for default values
|
|
321
|
+
const postFixture = createFixture(posts,
|
|
322
|
+
{ createdAt: () => Date.now() }, // template: default for createdAt
|
|
323
|
+
[
|
|
324
|
+
{ title: "First Post", userId: users.by.id(1) }, // RefBy
|
|
325
|
+
{ title: "Second Post", userId: users.by.id(1) },
|
|
326
|
+
],
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
// Pass to CowboyConnection
|
|
330
|
+
const connection = new CowboyConnection(rawConnection, schema, {
|
|
331
|
+
fixtures: { userFixture, postFixture },
|
|
332
|
+
runFixtures: true,
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
MIT
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"name": "@sigitex/outlaw",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"license": "MIT",
|
|
5
|
+
"description": "A trigger-happy sqlite framework.",
|
|
5
6
|
"author": {
|
|
6
7
|
"name": "Sigitex",
|
|
7
8
|
"url": "http://github.com/sigitex"
|
|
@@ -48,5 +49,5 @@
|
|
|
48
49
|
"files": [
|
|
49
50
|
"src"
|
|
50
51
|
],
|
|
51
|
-
"version": "1.0.
|
|
52
|
+
"version": "1.0.1"
|
|
52
53
|
}
|