@hammr/do-orm 1.0.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/README.md +560 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +358 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +4 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# DO-ORM
|
|
2
|
+
|
|
3
|
+
**Type-safe ORM for Cloudflare Durable Objects with zero runtime overhead**
|
|
4
|
+
|
|
5
|
+
DO-ORM makes Durable Objects queryable like a real database while maintaining the performance and simplicity of Cloudflare's storage API. Built with pure TypeScript, zero dependencies, and automatic schema validation.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
✅ **Type-safe schema definitions** - Full TypeScript inference for all CRUD operations
|
|
10
|
+
✅ **Automatic validation** - Schema validation on every write operation
|
|
11
|
+
✅ **Efficient indexing** - Single-field indexes for O(log n) queries instead of O(n) scans
|
|
12
|
+
✅ **Fluent query builder** - Chain `.where()`, `.after()`, `.before()`, `.limit()`, `.orderBy()`
|
|
13
|
+
✅ **Full CRUD support** - `create()`, `find()`, `update()`, `delete()`, and bulk operations
|
|
14
|
+
✅ **Zero dependencies** - Pure TypeScript using DO storage primitives
|
|
15
|
+
✅ **Zero runtime overhead** - Direct wrapper around Durable Objects storage API
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @hammr/do-orm
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Define your model
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { DOModel, SchemaDefinition, InferSchemaType } from '@hammr/do-orm';
|
|
29
|
+
|
|
30
|
+
// Define schema with type annotations
|
|
31
|
+
interface EventSchema extends SchemaDefinition {
|
|
32
|
+
id: 'string';
|
|
33
|
+
workspaceId: 'string';
|
|
34
|
+
timestamp: 'date';
|
|
35
|
+
type: 'string';
|
|
36
|
+
data: 'object';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create model class
|
|
40
|
+
class Event extends DOModel<EventSchema> {
|
|
41
|
+
protected schema: EventSchema = {
|
|
42
|
+
id: 'string',
|
|
43
|
+
workspaceId: 'string',
|
|
44
|
+
timestamp: 'date',
|
|
45
|
+
type: 'string',
|
|
46
|
+
data: 'object',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Define indexes for efficient queries
|
|
50
|
+
protected indexes = ['workspaceId', 'timestamp'] as const;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Use in your Durable Object
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export class MyDurableObject {
|
|
58
|
+
private eventModel: Event;
|
|
59
|
+
|
|
60
|
+
constructor(state: DurableObjectState) {
|
|
61
|
+
this.eventModel = new Event(state.storage);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async fetch(request: Request): Promise<Response> {
|
|
65
|
+
// Create an event
|
|
66
|
+
const event = await this.eventModel.create({
|
|
67
|
+
id: 'evt_123',
|
|
68
|
+
workspaceId: 'ws_abc',
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
type: 'click',
|
|
71
|
+
data: { button: 'submit' }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Query events
|
|
75
|
+
const recentEvents = await this.eventModel
|
|
76
|
+
.where({ workspaceId: 'ws_abc' })
|
|
77
|
+
.after(new Date('2024-01-01'))
|
|
78
|
+
.limit(100)
|
|
79
|
+
.orderBy('timestamp', 'desc')
|
|
80
|
+
.execute();
|
|
81
|
+
|
|
82
|
+
return new Response(JSON.stringify(recentEvents));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## API Reference
|
|
88
|
+
|
|
89
|
+
### Schema Types
|
|
90
|
+
|
|
91
|
+
DO-ORM supports the following field types:
|
|
92
|
+
|
|
93
|
+
- `'string'` - String values
|
|
94
|
+
- `'number'` - Numeric values (integers and floats)
|
|
95
|
+
- `'boolean'` - Boolean values (true/false)
|
|
96
|
+
- `'date'` - Date objects (automatically serialized/deserialized)
|
|
97
|
+
- `'object'` - Plain JavaScript objects
|
|
98
|
+
- `'array'` - Arrays of any type
|
|
99
|
+
|
|
100
|
+
### CRUD Operations
|
|
101
|
+
|
|
102
|
+
#### `create(data: T): Promise<T>`
|
|
103
|
+
|
|
104
|
+
Create a new record. Throws if validation fails or ID already exists.
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const event = await eventModel.create({
|
|
108
|
+
id: 'evt_1',
|
|
109
|
+
workspaceId: 'ws_abc',
|
|
110
|
+
timestamp: new Date(),
|
|
111
|
+
type: 'pageview',
|
|
112
|
+
data: { page: '/home' }
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### `find(id: string): Promise<T | null>`
|
|
117
|
+
|
|
118
|
+
Find a record by ID. Returns `null` if not found.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
const event = await eventModel.find('evt_1');
|
|
122
|
+
if (event) {
|
|
123
|
+
console.log(event.type); // Type-safe access
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### `update(id: string, updates: Partial<T>): Promise<T>`
|
|
128
|
+
|
|
129
|
+
Update a record with partial data. Validates the complete merged record.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const updated = await eventModel.update('evt_1', {
|
|
133
|
+
data: { page: '/about' }
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### `delete(id: string): Promise<boolean>`
|
|
138
|
+
|
|
139
|
+
Delete a record by ID. Returns `true` if deleted, `false` if not found.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const deleted = await eventModel.delete('evt_1');
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `all(): Promise<T[]>`
|
|
146
|
+
|
|
147
|
+
Get all records (unfiltered).
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
const allEvents = await eventModel.all();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### `count(): Promise<number>`
|
|
154
|
+
|
|
155
|
+
Count all records.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const totalEvents = await eventModel.count();
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Query Builder
|
|
162
|
+
|
|
163
|
+
Chain query methods for powerful filtering and sorting:
|
|
164
|
+
|
|
165
|
+
#### `where(conditions: Partial<T>): QueryBuilder<T>`
|
|
166
|
+
|
|
167
|
+
Filter by field values. Uses indexes when available.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
const events = await eventModel
|
|
171
|
+
.where({ workspaceId: 'ws_abc' })
|
|
172
|
+
.execute();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### `after(date: Date): QueryBuilder<T>`
|
|
176
|
+
|
|
177
|
+
Filter records with date fields after the specified date.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const recentEvents = await eventModel
|
|
181
|
+
.after(new Date('2024-01-01'))
|
|
182
|
+
.execute();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### `before(date: Date): QueryBuilder<T>`
|
|
186
|
+
|
|
187
|
+
Filter records with date fields before the specified date.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const oldEvents = await eventModel
|
|
191
|
+
.before(new Date('2023-12-31'))
|
|
192
|
+
.execute();
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `limit(count: number): QueryBuilder<T>`
|
|
196
|
+
|
|
197
|
+
Limit the number of results returned.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
const topEvents = await eventModel
|
|
201
|
+
.where({ workspaceId: 'ws_abc' })
|
|
202
|
+
.limit(10)
|
|
203
|
+
.execute();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `orderBy(field: keyof T, direction: 'asc' | 'desc'): QueryBuilder<T>`
|
|
207
|
+
|
|
208
|
+
Sort results by a field.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const sortedEvents = await eventModel
|
|
212
|
+
.where({ workspaceId: 'ws_abc' })
|
|
213
|
+
.orderBy('timestamp', 'desc')
|
|
214
|
+
.execute();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### `execute(): Promise<T[]>`
|
|
218
|
+
|
|
219
|
+
Execute the query and return results.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const events = await eventModel
|
|
223
|
+
.where({ workspaceId: 'ws_abc' })
|
|
224
|
+
.limit(100)
|
|
225
|
+
.execute();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Query Chaining Example
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const events = await eventModel
|
|
232
|
+
.where({ workspaceId: 'ws_abc' })
|
|
233
|
+
.after(new Date('2024-01-01'))
|
|
234
|
+
.before(new Date('2024-12-31'))
|
|
235
|
+
.orderBy('timestamp', 'desc')
|
|
236
|
+
.limit(50)
|
|
237
|
+
.execute();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Indexing
|
|
241
|
+
|
|
242
|
+
Indexes dramatically improve query performance by avoiding full table scans:
|
|
243
|
+
|
|
244
|
+
- **Without index**: O(n) - scans every record
|
|
245
|
+
- **With index**: O(log n) - uses sorted index lookup
|
|
246
|
+
|
|
247
|
+
### How to define indexes
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
class Event extends DOModel<EventSchema> {
|
|
251
|
+
protected schema: EventSchema = {
|
|
252
|
+
id: 'string',
|
|
253
|
+
workspaceId: 'string',
|
|
254
|
+
timestamp: 'date',
|
|
255
|
+
type: 'string',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Index these fields for efficient queries
|
|
259
|
+
protected indexes = ['workspaceId', 'timestamp'] as const;
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### When queries use indexes
|
|
264
|
+
|
|
265
|
+
- `.where({ indexedField: value })` - Uses index if first field is indexed
|
|
266
|
+
- Without indexed where clause - Falls back to full scan
|
|
267
|
+
|
|
268
|
+
### Index maintenance
|
|
269
|
+
|
|
270
|
+
Indexes are automatically maintained:
|
|
271
|
+
- Created during `create()`
|
|
272
|
+
- Updated during `update()` (if indexed fields change)
|
|
273
|
+
- Removed during `delete()`
|
|
274
|
+
|
|
275
|
+
## Schema Validation
|
|
276
|
+
|
|
277
|
+
DO-ORM validates all data against your schema:
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// ✅ Valid - passes validation
|
|
281
|
+
await eventModel.create({
|
|
282
|
+
id: 'evt_1',
|
|
283
|
+
workspaceId: 'ws_abc',
|
|
284
|
+
timestamp: new Date(),
|
|
285
|
+
type: 'click',
|
|
286
|
+
data: {}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ❌ Invalid - throws error
|
|
290
|
+
await eventModel.create({
|
|
291
|
+
id: 'evt_1',
|
|
292
|
+
workspaceId: 123, // Error: must be string
|
|
293
|
+
timestamp: new Date(),
|
|
294
|
+
type: 'click',
|
|
295
|
+
data: {}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ❌ Invalid - throws error
|
|
299
|
+
await eventModel.create({
|
|
300
|
+
id: 'evt_1',
|
|
301
|
+
// Missing required fields
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Validation errors
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
try {
|
|
309
|
+
await eventModel.create(invalidData);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
// "Field 'workspaceId' must be a string, got number"
|
|
312
|
+
// "Missing required field: timestamp"
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## TypeScript Inference
|
|
317
|
+
|
|
318
|
+
DO-ORM provides full type inference:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// Define schema
|
|
322
|
+
interface EventSchema extends SchemaDefinition {
|
|
323
|
+
id: 'string';
|
|
324
|
+
workspaceId: 'string';
|
|
325
|
+
timestamp: 'date';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
class Event extends DOModel<EventSchema> {
|
|
329
|
+
protected schema: EventSchema = {
|
|
330
|
+
id: 'string',
|
|
331
|
+
workspaceId: 'string',
|
|
332
|
+
timestamp: 'date',
|
|
333
|
+
};
|
|
334
|
+
protected indexes = ['workspaceId'] as const;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// TypeScript knows the exact type!
|
|
338
|
+
const event = await eventModel.find('evt_1');
|
|
339
|
+
// ^? Event | null
|
|
340
|
+
|
|
341
|
+
if (event) {
|
|
342
|
+
event.id; // string
|
|
343
|
+
event.workspaceId; // string
|
|
344
|
+
event.timestamp; // Date
|
|
345
|
+
event.unknown; // ❌ TypeScript error
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Advanced Usage
|
|
350
|
+
|
|
351
|
+
### Custom table names
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
class Event extends DOModel<EventSchema> {
|
|
355
|
+
constructor(storage: DurableObjectStorage) {
|
|
356
|
+
super(storage, 'custom_events_table');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
protected schema: EventSchema = { /* ... */ };
|
|
360
|
+
protected indexes = [] as const;
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Multiple models in one DO
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
export class MyDurableObject {
|
|
368
|
+
private events: Event;
|
|
369
|
+
private users: User;
|
|
370
|
+
|
|
371
|
+
constructor(state: DurableObjectState) {
|
|
372
|
+
this.events = new Event(state.storage);
|
|
373
|
+
this.users = new User(state.storage, 'users_table');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async fetch(request: Request): Promise<Response> {
|
|
377
|
+
const event = await this.events.find('evt_1');
|
|
378
|
+
const user = await this.users.find('user_1');
|
|
379
|
+
// ...
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Performance Considerations
|
|
385
|
+
|
|
386
|
+
### Index usage
|
|
387
|
+
|
|
388
|
+
- **Indexed queries**: Fast O(log n) lookups
|
|
389
|
+
- **Non-indexed queries**: Slower O(n) full scans
|
|
390
|
+
- **Best practice**: Index frequently queried fields
|
|
391
|
+
|
|
392
|
+
### Storage efficiency
|
|
393
|
+
|
|
394
|
+
- Records stored as: `{tableName}:{id}`
|
|
395
|
+
- Indexes stored as: `index:{tableName}:{field}:{value}`
|
|
396
|
+
- Dates serialized as ISO strings for efficient sorting
|
|
397
|
+
|
|
398
|
+
### Query optimization tips
|
|
399
|
+
|
|
400
|
+
1. **Use indexes** - Define indexes for frequently queried fields
|
|
401
|
+
2. **Limit results** - Always use `.limit()` for large datasets
|
|
402
|
+
3. **Specific where clauses** - Filter by indexed fields first
|
|
403
|
+
4. **Batch operations** - Consider batching writes for bulk inserts
|
|
404
|
+
|
|
405
|
+
## Limitations
|
|
406
|
+
|
|
407
|
+
- **No compound indexes** - Only single-field indexes (for now)
|
|
408
|
+
- **No transactions** - Each operation is atomic but not grouped
|
|
409
|
+
- **No joins** - Each model is independent
|
|
410
|
+
- **No migrations** - Schema changes require manual data migration
|
|
411
|
+
|
|
412
|
+
## Examples
|
|
413
|
+
|
|
414
|
+
### Analytics events tracker
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
interface AnalyticsSchema extends SchemaDefinition {
|
|
418
|
+
id: 'string';
|
|
419
|
+
sessionId: 'string';
|
|
420
|
+
userId: 'string';
|
|
421
|
+
event: 'string';
|
|
422
|
+
timestamp: 'date';
|
|
423
|
+
properties: 'object';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
class Analytics extends DOModel<AnalyticsSchema> {
|
|
427
|
+
protected schema: AnalyticsSchema = {
|
|
428
|
+
id: 'string',
|
|
429
|
+
sessionId: 'string',
|
|
430
|
+
userId: 'string',
|
|
431
|
+
event: 'string',
|
|
432
|
+
timestamp: 'date',
|
|
433
|
+
properties: 'object',
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
protected indexes = ['userId', 'sessionId', 'timestamp'] as const;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Track an event
|
|
440
|
+
await analytics.create({
|
|
441
|
+
id: generateId(),
|
|
442
|
+
sessionId: 'session_abc',
|
|
443
|
+
userId: 'user_123',
|
|
444
|
+
event: 'purchase',
|
|
445
|
+
timestamp: new Date(),
|
|
446
|
+
properties: { amount: 99.99, currency: 'USD' }
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Get user's recent events
|
|
450
|
+
const userEvents = await analytics
|
|
451
|
+
.where({ userId: 'user_123' })
|
|
452
|
+
.after(thirtyDaysAgo)
|
|
453
|
+
.orderBy('timestamp', 'desc')
|
|
454
|
+
.limit(100)
|
|
455
|
+
.execute();
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Task queue
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
interface TaskSchema extends SchemaDefinition {
|
|
462
|
+
id: 'string';
|
|
463
|
+
status: 'string';
|
|
464
|
+
priority: 'number';
|
|
465
|
+
createdAt: 'date';
|
|
466
|
+
payload: 'object';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
class Task extends DOModel<TaskSchema> {
|
|
470
|
+
protected schema: TaskSchema = {
|
|
471
|
+
id: 'string',
|
|
472
|
+
status: 'string',
|
|
473
|
+
priority: 'number',
|
|
474
|
+
createdAt: 'date',
|
|
475
|
+
payload: 'object',
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
protected indexes = ['status', 'priority'] as const;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Add task
|
|
482
|
+
await task.create({
|
|
483
|
+
id: 'task_1',
|
|
484
|
+
status: 'pending',
|
|
485
|
+
priority: 1,
|
|
486
|
+
createdAt: new Date(),
|
|
487
|
+
payload: { action: 'send_email' }
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Get pending tasks
|
|
491
|
+
const pending = await task
|
|
492
|
+
.where({ status: 'pending' })
|
|
493
|
+
.orderBy('priority', 'asc')
|
|
494
|
+
.limit(10)
|
|
495
|
+
.execute();
|
|
496
|
+
|
|
497
|
+
// Process and mark complete
|
|
498
|
+
for (const t of pending) {
|
|
499
|
+
await processTask(t);
|
|
500
|
+
await task.update(t.id, { status: 'completed' });
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Testing
|
|
505
|
+
|
|
506
|
+
### Unit Tests
|
|
507
|
+
|
|
508
|
+
Run the unit test suite:
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
npm test
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Tests include:
|
|
515
|
+
- Schema validation (type checking, required fields)
|
|
516
|
+
- CRUD operations (create, read, update, delete)
|
|
517
|
+
- Query builder (where, limit, orderBy, date ranges)
|
|
518
|
+
- Index usage and maintenance
|
|
519
|
+
- Edge cases (duplicates, missing records)
|
|
520
|
+
|
|
521
|
+
### Integration Tests (with Cloudflare Workers)
|
|
522
|
+
|
|
523
|
+
Test the ORM in a real Cloudflare Workers environment:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
# Terminal 1: Start the worker
|
|
527
|
+
npm run dev
|
|
528
|
+
|
|
529
|
+
# Terminal 2: Run integration tests
|
|
530
|
+
npm run test:worker
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
The integration tests verify the complete stack:
|
|
534
|
+
- Worker HTTP endpoints
|
|
535
|
+
- Durable Object instantiation
|
|
536
|
+
- DO-ORM with real DO storage
|
|
537
|
+
- Schema validation in production
|
|
538
|
+
- Query performance with indexes
|
|
539
|
+
|
|
540
|
+
See [TESTING.md](./TESTING.md) for more details on testing with Cloudflare Workers.
|
|
541
|
+
|
|
542
|
+
## Contributing
|
|
543
|
+
|
|
544
|
+
This is v1 - there's lots of room for improvement!
|
|
545
|
+
|
|
546
|
+
**Potential enhancements:**
|
|
547
|
+
- Compound indexes (multiple fields)
|
|
548
|
+
- Transactions support
|
|
549
|
+
- Query result streaming
|
|
550
|
+
- Migration helpers
|
|
551
|
+
- Soft deletes
|
|
552
|
+
- Hooks (beforeCreate, afterUpdate, etc.)
|
|
553
|
+
|
|
554
|
+
## License
|
|
555
|
+
|
|
556
|
+
MIT
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
Built for the Cloudflare Workers ecosystem. Works seamlessly with Durable Objects and provides a better developer experience than raw storage API calls.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DO-ORM v1 - Type-safe ORM for Cloudflare Durable Objects
|
|
3
|
+
* Zero dependencies, pure TypeScript implementation
|
|
4
|
+
*/
|
|
5
|
+
import type { SchemaDefinition, QueryOptions, InferSchemaType } from './types';
|
|
6
|
+
export * from './types';
|
|
7
|
+
/**
|
|
8
|
+
* Base class for all DO models
|
|
9
|
+
* Provides CRUD operations, schema validation, and indexing
|
|
10
|
+
*/
|
|
11
|
+
export declare abstract class DOModel<S extends SchemaDefinition> {
|
|
12
|
+
protected abstract schema: S;
|
|
13
|
+
protected abstract indexes: (keyof InferSchemaType<S>)[];
|
|
14
|
+
protected storage: DurableObjectStorage;
|
|
15
|
+
protected tableName: string;
|
|
16
|
+
constructor(storage: DurableObjectStorage, tableName?: string);
|
|
17
|
+
/**
|
|
18
|
+
* Validate a value against a schema field type
|
|
19
|
+
*/
|
|
20
|
+
private validateField;
|
|
21
|
+
/**
|
|
22
|
+
* Validate an entire record against the schema
|
|
23
|
+
*/
|
|
24
|
+
private validateSchema;
|
|
25
|
+
/**
|
|
26
|
+
* Generate storage key for a record
|
|
27
|
+
*/
|
|
28
|
+
private getRecordKey;
|
|
29
|
+
/**
|
|
30
|
+
* Generate storage key for an index
|
|
31
|
+
*/
|
|
32
|
+
private getIndexKey;
|
|
33
|
+
/**
|
|
34
|
+
* Get all index entry keys for a given field
|
|
35
|
+
*/
|
|
36
|
+
private getIndexPrefix;
|
|
37
|
+
/**
|
|
38
|
+
* Serialize a value for storage (convert Dates to ISO strings)
|
|
39
|
+
*/
|
|
40
|
+
private serialize;
|
|
41
|
+
/**
|
|
42
|
+
* Deserialize a value from storage (convert ISO strings back to Dates)
|
|
43
|
+
*/
|
|
44
|
+
private deserialize;
|
|
45
|
+
/**
|
|
46
|
+
* Update indexes for a record
|
|
47
|
+
*/
|
|
48
|
+
private updateIndexes;
|
|
49
|
+
/**
|
|
50
|
+
* Remove record ID from indexes
|
|
51
|
+
*/
|
|
52
|
+
private removeFromIndexes;
|
|
53
|
+
/**
|
|
54
|
+
* Create a new record
|
|
55
|
+
*/
|
|
56
|
+
create(data: InferSchemaType<S>): Promise<InferSchemaType<S>>;
|
|
57
|
+
/**
|
|
58
|
+
* Find a record by ID
|
|
59
|
+
*/
|
|
60
|
+
find(id: string): Promise<InferSchemaType<S> | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Update a record
|
|
63
|
+
*/
|
|
64
|
+
update(id: string, updates: Partial<InferSchemaType<S>>): Promise<InferSchemaType<S>>;
|
|
65
|
+
/**
|
|
66
|
+
* Delete a record
|
|
67
|
+
*/
|
|
68
|
+
delete(id: string): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Query builder - returns all matching records
|
|
71
|
+
*/
|
|
72
|
+
query(options?: QueryOptions<InferSchemaType<S>>): Promise<InferSchemaType<S>[]>;
|
|
73
|
+
/**
|
|
74
|
+
* Query builder with fluent API
|
|
75
|
+
*/
|
|
76
|
+
where(conditions: Partial<InferSchemaType<S>>): QueryBuilder<S>;
|
|
77
|
+
/**
|
|
78
|
+
* Get all records
|
|
79
|
+
*/
|
|
80
|
+
all(): Promise<InferSchemaType<S>[]>;
|
|
81
|
+
/**
|
|
82
|
+
* Count all records
|
|
83
|
+
*/
|
|
84
|
+
count(): Promise<number>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fluent query builder
|
|
88
|
+
*/
|
|
89
|
+
export declare class QueryBuilder<S extends SchemaDefinition> {
|
|
90
|
+
private model;
|
|
91
|
+
private options;
|
|
92
|
+
constructor(model: DOModel<S>, options?: QueryOptions<InferSchemaType<S>>);
|
|
93
|
+
where(conditions: Partial<InferSchemaType<S>>): QueryBuilder<S>;
|
|
94
|
+
after(date: Date): QueryBuilder<S>;
|
|
95
|
+
before(date: Date): QueryBuilder<S>;
|
|
96
|
+
limit(count: number): QueryBuilder<S>;
|
|
97
|
+
orderBy(field: keyof InferSchemaType<S>, direction?: 'asc' | 'desc'): QueryBuilder<S>;
|
|
98
|
+
execute(): Promise<InferSchemaType<S>[]>;
|
|
99
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DO-ORM v1 - Type-safe ORM for Cloudflare Durable Objects
|
|
3
|
+
* Zero dependencies, pure TypeScript implementation
|
|
4
|
+
*/
|
|
5
|
+
export * from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Base class for all DO models
|
|
8
|
+
* Provides CRUD operations, schema validation, and indexing
|
|
9
|
+
*/
|
|
10
|
+
export class DOModel {
|
|
11
|
+
constructor(storage, tableName) {
|
|
12
|
+
this.storage = storage;
|
|
13
|
+
this.tableName = tableName || this.constructor.name.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validate a value against a schema field type
|
|
17
|
+
*/
|
|
18
|
+
validateField(value, fieldType, fieldName) {
|
|
19
|
+
switch (fieldType) {
|
|
20
|
+
case 'string':
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
throw new Error(`Field '${fieldName}' must be a string, got ${typeof value}`);
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
case 'number':
|
|
26
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
27
|
+
throw new Error(`Field '${fieldName}' must be a number, got ${typeof value}`);
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case 'boolean':
|
|
31
|
+
if (typeof value !== 'boolean') {
|
|
32
|
+
throw new Error(`Field '${fieldName}' must be a boolean, got ${typeof value}`);
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'date':
|
|
36
|
+
if (!(value instanceof Date) || isNaN(value.getTime())) {
|
|
37
|
+
throw new Error(`Field '${fieldName}' must be a valid Date, got ${typeof value}`);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case 'object':
|
|
41
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
42
|
+
throw new Error(`Field '${fieldName}' must be an object, got ${typeof value}`);
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case 'array':
|
|
46
|
+
if (!Array.isArray(value)) {
|
|
47
|
+
throw new Error(`Field '${fieldName}' must be an array, got ${typeof value}`);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unknown field type: ${fieldType}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate an entire record against the schema
|
|
56
|
+
*/
|
|
57
|
+
validateSchema(data) {
|
|
58
|
+
// Check for missing required fields
|
|
59
|
+
for (const [fieldName, fieldType] of Object.entries(this.schema)) {
|
|
60
|
+
if (!(fieldName in data)) {
|
|
61
|
+
throw new Error(`Missing required field: ${fieldName}`);
|
|
62
|
+
}
|
|
63
|
+
this.validateField(data[fieldName], fieldType, fieldName);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate storage key for a record
|
|
68
|
+
*/
|
|
69
|
+
getRecordKey(id) {
|
|
70
|
+
return `${this.tableName}:${id}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate storage key for an index
|
|
74
|
+
*/
|
|
75
|
+
getIndexKey(field, value) {
|
|
76
|
+
const normalizedValue = value instanceof Date ? value.toISOString() : String(value);
|
|
77
|
+
return `index:${this.tableName}:${field}:${normalizedValue}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get all index entry keys for a given field
|
|
81
|
+
*/
|
|
82
|
+
getIndexPrefix(field) {
|
|
83
|
+
return `index:${this.tableName}:${field}:`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Serialize a value for storage (convert Dates to ISO strings)
|
|
87
|
+
*/
|
|
88
|
+
serialize(data) {
|
|
89
|
+
const serialized = {};
|
|
90
|
+
for (const [key, value] of Object.entries(data)) {
|
|
91
|
+
if (value instanceof Date) {
|
|
92
|
+
serialized[key] = value.toISOString();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
serialized[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return serialized;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Deserialize a value from storage (convert ISO strings back to Dates)
|
|
102
|
+
*/
|
|
103
|
+
deserialize(data) {
|
|
104
|
+
const deserialized = {};
|
|
105
|
+
for (const [key, value] of Object.entries(data)) {
|
|
106
|
+
const fieldType = this.schema[key];
|
|
107
|
+
if (fieldType === 'date' && typeof value === 'string') {
|
|
108
|
+
deserialized[key] = new Date(value);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
deserialized[key] = value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return deserialized;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Update indexes for a record
|
|
118
|
+
*/
|
|
119
|
+
async updateIndexes(id, data) {
|
|
120
|
+
for (const indexField of this.indexes) {
|
|
121
|
+
const fieldValue = data[indexField];
|
|
122
|
+
const indexKey = this.getIndexKey(String(indexField), fieldValue);
|
|
123
|
+
// Get existing IDs in this index
|
|
124
|
+
const existingIds = await this.storage.get(indexKey) || [];
|
|
125
|
+
// Add ID if not already present
|
|
126
|
+
if (!existingIds.includes(id)) {
|
|
127
|
+
existingIds.push(id);
|
|
128
|
+
await this.storage.put(indexKey, existingIds);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Remove record ID from indexes
|
|
134
|
+
*/
|
|
135
|
+
async removeFromIndexes(id, data) {
|
|
136
|
+
for (const indexField of this.indexes) {
|
|
137
|
+
const fieldValue = data[indexField];
|
|
138
|
+
const indexKey = this.getIndexKey(String(indexField), fieldValue);
|
|
139
|
+
// Get existing IDs and remove this one
|
|
140
|
+
const existingIds = await this.storage.get(indexKey) || [];
|
|
141
|
+
const filteredIds = existingIds.filter(existingId => existingId !== id);
|
|
142
|
+
if (filteredIds.length > 0) {
|
|
143
|
+
await this.storage.put(indexKey, filteredIds);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
await this.storage.delete(indexKey);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create a new record
|
|
152
|
+
*/
|
|
153
|
+
async create(data) {
|
|
154
|
+
// Validate schema
|
|
155
|
+
this.validateSchema(data);
|
|
156
|
+
// Check if record already exists
|
|
157
|
+
const id = data.id;
|
|
158
|
+
if (!id) {
|
|
159
|
+
throw new Error('Record must have an id field');
|
|
160
|
+
}
|
|
161
|
+
const key = this.getRecordKey(id);
|
|
162
|
+
const existing = await this.storage.get(key);
|
|
163
|
+
if (existing) {
|
|
164
|
+
throw new Error(`Record with id '${id}' already exists`);
|
|
165
|
+
}
|
|
166
|
+
// Serialize and store
|
|
167
|
+
const serialized = this.serialize(data);
|
|
168
|
+
await this.storage.put(key, serialized);
|
|
169
|
+
// Update indexes
|
|
170
|
+
await this.updateIndexes(id, data);
|
|
171
|
+
return data;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Find a record by ID
|
|
175
|
+
*/
|
|
176
|
+
async find(id) {
|
|
177
|
+
const key = this.getRecordKey(id);
|
|
178
|
+
const data = await this.storage.get(key);
|
|
179
|
+
if (!data) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return this.deserialize(data);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Update a record
|
|
186
|
+
*/
|
|
187
|
+
async update(id, updates) {
|
|
188
|
+
const existing = await this.find(id);
|
|
189
|
+
if (!existing) {
|
|
190
|
+
throw new Error(`Record with id '${id}' not found`);
|
|
191
|
+
}
|
|
192
|
+
// Merge updates
|
|
193
|
+
const updated = { ...existing, ...updates };
|
|
194
|
+
// Validate the complete record
|
|
195
|
+
this.validateSchema(updated);
|
|
196
|
+
// Remove old indexes if indexed fields changed
|
|
197
|
+
const indexedFieldsChanged = this.indexes.some(field => field in updates && updates[field] !== existing[field]);
|
|
198
|
+
if (indexedFieldsChanged) {
|
|
199
|
+
await this.removeFromIndexes(id, existing);
|
|
200
|
+
}
|
|
201
|
+
// Store updated record
|
|
202
|
+
const serialized = this.serialize(updated);
|
|
203
|
+
await this.storage.put(this.getRecordKey(id), serialized);
|
|
204
|
+
// Update indexes with new values
|
|
205
|
+
if (indexedFieldsChanged) {
|
|
206
|
+
await this.updateIndexes(id, updated);
|
|
207
|
+
}
|
|
208
|
+
return updated;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Delete a record
|
|
212
|
+
*/
|
|
213
|
+
async delete(id) {
|
|
214
|
+
const existing = await this.find(id);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
// Remove from indexes
|
|
219
|
+
await this.removeFromIndexes(id, existing);
|
|
220
|
+
// Delete record
|
|
221
|
+
await this.storage.delete(this.getRecordKey(id));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Query builder - returns all matching records
|
|
226
|
+
*/
|
|
227
|
+
async query(options = {}) {
|
|
228
|
+
let candidateIds = [];
|
|
229
|
+
// Use indexes if where clause matches an indexed field
|
|
230
|
+
if (options.where) {
|
|
231
|
+
const whereEntries = Object.entries(options.where);
|
|
232
|
+
if (whereEntries.length > 0) {
|
|
233
|
+
const [field, value] = whereEntries[0];
|
|
234
|
+
// Check if this field is indexed
|
|
235
|
+
if (this.indexes.includes(field)) {
|
|
236
|
+
const indexKey = this.getIndexKey(field, value);
|
|
237
|
+
candidateIds = await this.storage.get(indexKey) || [];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// If no index was used, scan all records (slower)
|
|
242
|
+
if (candidateIds.length === 0 && !options.where) {
|
|
243
|
+
const prefix = `${this.tableName}:`;
|
|
244
|
+
const allKeys = await this.storage.list({ prefix });
|
|
245
|
+
candidateIds = Array.from(allKeys.keys()).map(key => key.toString().replace(prefix, ''));
|
|
246
|
+
}
|
|
247
|
+
// Load all candidate records
|
|
248
|
+
const records = [];
|
|
249
|
+
for (const id of candidateIds) {
|
|
250
|
+
const record = await this.find(id);
|
|
251
|
+
if (record) {
|
|
252
|
+
records.push(record);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Apply filters
|
|
256
|
+
let filtered = records;
|
|
257
|
+
// Filter by where clause (additional fields not covered by index)
|
|
258
|
+
if (options.where) {
|
|
259
|
+
filtered = filtered.filter(record => {
|
|
260
|
+
for (const [key, value] of Object.entries(options.where)) {
|
|
261
|
+
if (record[key] !== value) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// Filter by date range (after/before)
|
|
269
|
+
if (options.after || options.before) {
|
|
270
|
+
filtered = filtered.filter(record => {
|
|
271
|
+
// Find date field in schema
|
|
272
|
+
for (const [field, type] of Object.entries(this.schema)) {
|
|
273
|
+
if (type === 'date') {
|
|
274
|
+
const dateValue = record[field];
|
|
275
|
+
if (dateValue instanceof Date) {
|
|
276
|
+
if (options.after && dateValue <= options.after)
|
|
277
|
+
return false;
|
|
278
|
+
if (options.before && dateValue >= options.before)
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Sort
|
|
287
|
+
if (options.orderBy) {
|
|
288
|
+
const { field, direction } = options.orderBy;
|
|
289
|
+
filtered.sort((a, b) => {
|
|
290
|
+
const aVal = a[field];
|
|
291
|
+
const bVal = b[field];
|
|
292
|
+
let comparison = 0;
|
|
293
|
+
if (aVal < bVal)
|
|
294
|
+
comparison = -1;
|
|
295
|
+
if (aVal > bVal)
|
|
296
|
+
comparison = 1;
|
|
297
|
+
return direction === 'desc' ? -comparison : comparison;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Apply limit
|
|
301
|
+
if (options.limit) {
|
|
302
|
+
filtered = filtered.slice(0, options.limit);
|
|
303
|
+
}
|
|
304
|
+
return filtered;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Query builder with fluent API
|
|
308
|
+
*/
|
|
309
|
+
where(conditions) {
|
|
310
|
+
return new QueryBuilder(this, { where: conditions });
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get all records
|
|
314
|
+
*/
|
|
315
|
+
async all() {
|
|
316
|
+
return this.query();
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Count all records
|
|
320
|
+
*/
|
|
321
|
+
async count() {
|
|
322
|
+
const prefix = `${this.tableName}:`;
|
|
323
|
+
const allKeys = await this.storage.list({ prefix });
|
|
324
|
+
return allKeys.size;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Fluent query builder
|
|
329
|
+
*/
|
|
330
|
+
export class QueryBuilder {
|
|
331
|
+
constructor(model, options = {}) {
|
|
332
|
+
this.model = model;
|
|
333
|
+
this.options = options;
|
|
334
|
+
}
|
|
335
|
+
where(conditions) {
|
|
336
|
+
this.options.where = { ...this.options.where, ...conditions };
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
after(date) {
|
|
340
|
+
this.options.after = date;
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
before(date) {
|
|
344
|
+
this.options.before = date;
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
limit(count) {
|
|
348
|
+
this.options.limit = count;
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
351
|
+
orderBy(field, direction = 'asc') {
|
|
352
|
+
this.options.orderBy = { field, direction };
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
async execute() {
|
|
356
|
+
return this.model.query(this.options);
|
|
357
|
+
}
|
|
358
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for DO-ORM
|
|
3
|
+
*/
|
|
4
|
+
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array';
|
|
5
|
+
export interface SchemaDefinition {
|
|
6
|
+
[key: string]: FieldType;
|
|
7
|
+
}
|
|
8
|
+
export interface QueryOptions<T> {
|
|
9
|
+
where?: Partial<T>;
|
|
10
|
+
after?: Date;
|
|
11
|
+
before?: Date;
|
|
12
|
+
limit?: number;
|
|
13
|
+
orderBy?: {
|
|
14
|
+
field: keyof T;
|
|
15
|
+
direction: 'asc' | 'desc';
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface ModelConfig {
|
|
19
|
+
tableName?: string;
|
|
20
|
+
}
|
|
21
|
+
export type InferSchemaType<S extends SchemaDefinition> = {
|
|
22
|
+
[K in keyof S]: S[K] extends 'string' ? string : S[K] extends 'number' ? number : S[K] extends 'boolean' ? boolean : S[K] extends 'date' ? Date : S[K] extends 'object' ? Record<string, any> : S[K] extends 'array' ? any[] : never;
|
|
23
|
+
};
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hammr/do-orm",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Type-safe ORM for Cloudflare Durable Objects with zero runtime overhead",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "tsx test.ts",
|
|
10
|
+
"dev": "wrangler dev",
|
|
11
|
+
"deploy": "wrangler deploy",
|
|
12
|
+
"test:worker": "tsx test-worker.ts",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cloudflare",
|
|
17
|
+
"durable-objects",
|
|
18
|
+
"orm",
|
|
19
|
+
"typescript",
|
|
20
|
+
"database",
|
|
21
|
+
"stateless"
|
|
22
|
+
],
|
|
23
|
+
"author": "Edge Foundry, Inc.",
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@cloudflare/workers-types": "^4.20231218.0",
|
|
27
|
+
"typescript": "^5.3.3",
|
|
28
|
+
"tsx": "^4.7.0",
|
|
29
|
+
"wrangler": "^3.78.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@cloudflare/workers-types": ">=4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
]
|
|
38
|
+
}
|