@workglow/storage 0.0.52

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.
Files changed (74) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1015 -0
  3. package/dist/browser.js +2635 -0
  4. package/dist/browser.js.map +27 -0
  5. package/dist/bun.js +3880 -0
  6. package/dist/bun.js.map +35 -0
  7. package/dist/common-server.d.ts +23 -0
  8. package/dist/common-server.d.ts.map +1 -0
  9. package/dist/common.d.ts +16 -0
  10. package/dist/common.d.ts.map +1 -0
  11. package/dist/kv/FsFolderJsonKvRepository.d.ts +27 -0
  12. package/dist/kv/FsFolderJsonKvRepository.d.ts.map +1 -0
  13. package/dist/kv/FsFolderKvRepository.d.ts +74 -0
  14. package/dist/kv/FsFolderKvRepository.d.ts.map +1 -0
  15. package/dist/kv/IKvRepository.d.ts +65 -0
  16. package/dist/kv/IKvRepository.d.ts.map +1 -0
  17. package/dist/kv/InMemoryKvRepository.d.ts +26 -0
  18. package/dist/kv/InMemoryKvRepository.d.ts.map +1 -0
  19. package/dist/kv/IndexedDbKvRepository.d.ts +27 -0
  20. package/dist/kv/IndexedDbKvRepository.d.ts.map +1 -0
  21. package/dist/kv/KvRepository.d.ts +109 -0
  22. package/dist/kv/KvRepository.d.ts.map +1 -0
  23. package/dist/kv/KvViaTabularRepository.d.ts +64 -0
  24. package/dist/kv/KvViaTabularRepository.d.ts.map +1 -0
  25. package/dist/kv/PostgresKvRepository.d.ts +28 -0
  26. package/dist/kv/PostgresKvRepository.d.ts.map +1 -0
  27. package/dist/kv/SqliteKvRepository.d.ts +28 -0
  28. package/dist/kv/SqliteKvRepository.d.ts.map +1 -0
  29. package/dist/kv/SupabaseKvRepository.d.ts +34 -0
  30. package/dist/kv/SupabaseKvRepository.d.ts.map +1 -0
  31. package/dist/node.js +3879 -0
  32. package/dist/node.js.map +35 -0
  33. package/dist/queue/IQueueStorage.d.ts +125 -0
  34. package/dist/queue/IQueueStorage.d.ts.map +1 -0
  35. package/dist/queue/InMemoryQueueStorage.d.ts +109 -0
  36. package/dist/queue/InMemoryQueueStorage.d.ts.map +1 -0
  37. package/dist/queue/IndexedDbQueueStorage.d.ts +89 -0
  38. package/dist/queue/IndexedDbQueueStorage.d.ts.map +1 -0
  39. package/dist/queue/PostgresQueueStorage.d.ts +92 -0
  40. package/dist/queue/PostgresQueueStorage.d.ts.map +1 -0
  41. package/dist/queue/SqliteQueueStorage.d.ts +116 -0
  42. package/dist/queue/SqliteQueueStorage.d.ts.map +1 -0
  43. package/dist/queue/SupabaseQueueStorage.d.ts +93 -0
  44. package/dist/queue/SupabaseQueueStorage.d.ts.map +1 -0
  45. package/dist/tabular/BaseSqlTabularRepository.d.ts +94 -0
  46. package/dist/tabular/BaseSqlTabularRepository.d.ts.map +1 -0
  47. package/dist/tabular/CachedTabularRepository.d.ts +110 -0
  48. package/dist/tabular/CachedTabularRepository.d.ts.map +1 -0
  49. package/dist/tabular/FsFolderTabularRepository.d.ts +92 -0
  50. package/dist/tabular/FsFolderTabularRepository.d.ts.map +1 -0
  51. package/dist/tabular/ITabularRepository.d.ts +52 -0
  52. package/dist/tabular/ITabularRepository.d.ts.map +1 -0
  53. package/dist/tabular/InMemoryTabularRepository.d.ts +93 -0
  54. package/dist/tabular/InMemoryTabularRepository.d.ts.map +1 -0
  55. package/dist/tabular/IndexedDbTabularRepository.d.ts +100 -0
  56. package/dist/tabular/IndexedDbTabularRepository.d.ts.map +1 -0
  57. package/dist/tabular/PostgresTabularRepository.d.ts +133 -0
  58. package/dist/tabular/PostgresTabularRepository.d.ts.map +1 -0
  59. package/dist/tabular/SharedInMemoryTabularRepository.d.ts +126 -0
  60. package/dist/tabular/SharedInMemoryTabularRepository.d.ts.map +1 -0
  61. package/dist/tabular/SqliteTabularRepository.d.ts +110 -0
  62. package/dist/tabular/SqliteTabularRepository.d.ts.map +1 -0
  63. package/dist/tabular/SupabaseTabularRepository.d.ts +132 -0
  64. package/dist/tabular/SupabaseTabularRepository.d.ts.map +1 -0
  65. package/dist/tabular/TabularRepository.d.ts +123 -0
  66. package/dist/tabular/TabularRepository.d.ts.map +1 -0
  67. package/dist/types.d.ts +7 -0
  68. package/dist/types.d.ts.map +1 -0
  69. package/dist/util/IndexedDbTable.d.ts +40 -0
  70. package/dist/util/IndexedDbTable.d.ts.map +1 -0
  71. package/package.json +60 -0
  72. package/src/kv/README.md +159 -0
  73. package/src/queue/README.md +41 -0
  74. package/src/tabular/README.md +298 -0
package/README.md ADDED
@@ -0,0 +1,1015 @@
1
+ # @workglow/storage
2
+
3
+ Modular storage solutions for Workglow.AI platform with multiple backend implementations. Provides consistent interfaces for key-value storage, tabular data storage, and job queue persistence.
4
+
5
+ - [Quick Start](#quick-start)
6
+ - [Installation](#installation)
7
+ - [Core Concepts](#core-concepts)
8
+ - [Type Safety](#type-safety)
9
+ - [Environment Compatibility](#environment-compatibility)
10
+ - [Import Patterns](#import-patterns)
11
+ - [Storage Types](#storage-types)
12
+ - [Key-Value Storage](#key-value-storage)
13
+ - [Basic Usage](#basic-usage)
14
+ - [Environment-Specific Examples](#environment-specific-examples)
15
+ - [Bulk Operations](#bulk-operations)
16
+ - [Event Handling](#event-handling)
17
+ - [Tabular Storage](#tabular-storage)
18
+ - [Schema Definition](#schema-definition)
19
+ - [CRUD Operations](#crud-operations)
20
+ - [Bulk Operations](#bulk-operations-1)
21
+ - [Searching and Filtering](#searching-and-filtering)
22
+ - [Environment-Specific Tabular Storage](#environment-specific-tabular-storage)
23
+ - [Queue Storage](#queue-storage)
24
+ - [Basic Job Queue Operations](#basic-job-queue-operations)
25
+ - [Job Management](#job-management)
26
+ - [Environment-Specific Usage](#environment-specific-usage)
27
+ - [Browser Environment](#browser-environment)
28
+ - [Node.js Environment](#nodejs-environment)
29
+ - [Bun Environment](#bun-environment)
30
+ - [Advanced Features](#advanced-features)
31
+ - [Event-Driven Architecture](#event-driven-architecture)
32
+ - [Compound Primary Keys](#compound-primary-keys)
33
+ - [Custom File Layout (KV on filesystem)](#custom-file-layout-kv-on-filesystem)
34
+ - [API Reference](#api-reference)
35
+ - [IKvRepository\<Key, Value\>](#ikvrepositorykey-value)
36
+ - [ITabularRepository\<Schema, PrimaryKeyNames\>](#itabularrepositoryschema-primarykeynames)
37
+ - [IQueueStorage\<Input, Output\>](#iqueuestorageinput-output)
38
+ - [Examples](#examples)
39
+ - [User Management System](#user-management-system)
40
+ - [Configuration Management](#configuration-management)
41
+ - [Testing](#testing)
42
+ - [Writing Tests for Your Storage Usage](#writing-tests-for-your-storage-usage)
43
+ - [License](#license)
44
+
45
+ ## Quick Start
46
+
47
+ ```typescript
48
+ // Key-Value Storage (simple data)
49
+ import { InMemoryKvRepository } from "@workglow/storage";
50
+
51
+ const kvStore = new InMemoryKvRepository<string, { name: string; age: number }>();
52
+ await kvStore.put("user:123", { name: "Alice", age: 30 });
53
+ const kvUser = await kvStore.get("user:123"); // { name: "Alice", age: 30 }
54
+ ```
55
+
56
+ ```typescript
57
+ // Tabular Storage (structured data with schemas)
58
+ import { InMemoryTabularRepository } from "@workglow/storage";
59
+ import { JsonSchema } from "@workglow/util";
60
+
61
+ const userSchema = {
62
+ type: "object",
63
+ properties: {
64
+ id: { type: "string" },
65
+ name: { type: "string" },
66
+ email: { type: "string" },
67
+ age: { type: "number" },
68
+ },
69
+ required: ["id", "name", "email", "age"],
70
+ additionalProperties: false,
71
+ } as const satisfies JsonSchema;
72
+
73
+ const userRepo = new InMemoryTabularRepository<typeof userSchema, ["id"]>(
74
+ userSchema,
75
+ ["id"], // primary key
76
+ ["email"] // additional indexes
77
+ );
78
+
79
+ await userRepo.put({ id: "123", name: "Alice", email: "alice@example.com", age: 30 });
80
+ const user = await userRepo.get({ id: "123" });
81
+ ```
82
+
83
+ ## Installation
84
+
85
+ ```bash
86
+ # Using bun (recommended)
87
+ bun install @workglow/storage
88
+
89
+ # Using npm
90
+ npm install @workglow/storage
91
+
92
+ # Using yarn
93
+ yarn add @workglow/storage
94
+ ```
95
+
96
+ ## Core Concepts
97
+
98
+ ### Type Safety
99
+
100
+ All storage implementations are fully typed using TypeScript and JSON Schema for runtime validation:
101
+
102
+ ```typescript
103
+ import { JsonSchema, FromSchema } from "@workglow/util";
104
+
105
+ // Define your data structure
106
+ const ProductSchema = {
107
+ type: "object",
108
+ properties: {
109
+ id: { type: "string" },
110
+ name: { type: "string" },
111
+ price: { type: "number" },
112
+ category: { type: "string" },
113
+ inStock: { type: "boolean" },
114
+ },
115
+ required: ["id", "name", "price", "category", "inStock"],
116
+ additionalProperties: false,
117
+ } as const satisfies JsonSchema;
118
+
119
+ // TypeScript automatically infers:
120
+ // Entity = FromSchema<typeof ProductSchema>
121
+ // PrimaryKey = { id: string }
122
+ ```
123
+
124
+ ### Environment Compatibility
125
+
126
+ | Storage Type | Node.js | Bun | Browser | Persistence |
127
+ | ------------ | ------- | --- | ------- | ----------- |
128
+ | InMemory | ✅ | ✅ | ✅ | ❌ |
129
+ | IndexedDB | ❌ | ❌ | ✅ | ✅ |
130
+ | SQLite | ✅ | ✅ | ❌ | ✅ |
131
+ | PostgreSQL | ✅ | ✅ | ❌ | ✅ |
132
+ | Supabase | ✅ | ✅ | ✅ | ✅ |
133
+ | FileSystem | ✅ | ✅ | ❌ | ✅ |
134
+
135
+ ### Import Patterns
136
+
137
+ The package uses conditional exports, so importing from `@workglow/storage` automatically selects the right build for your runtime (browser, Node.js, or Bun).
138
+
139
+ ```typescript
140
+ // Import from the top-level package; it resolves to the correct target per environment
141
+ import { InMemoryKvRepository, SqliteTabularRepository } from "@workglow/storage";
142
+ ```
143
+
144
+ ## Storage Types
145
+
146
+ ### Key-Value Storage
147
+
148
+ Simple key-value storage for unstructured or semi-structured data.
149
+
150
+ #### Basic Usage
151
+
152
+ ```typescript
153
+ import { InMemoryKvRepository, FsFolderJsonKvRepository } from "@workglow/storage";
154
+
155
+ // In-memory (for testing/caching)
156
+ const cache = new InMemoryKvRepository<string, any>();
157
+ await cache.put("config", { theme: "dark", language: "en" });
158
+
159
+ // File-based JSON (persistent)
160
+ const settings = new FsFolderJsonKvRepository("./data/settings");
161
+ await settings.put("user:preferences", { notifications: true });
162
+ ```
163
+
164
+ #### Environment-Specific Examples
165
+
166
+ ```typescript
167
+ // Browser (using IndexedDB)
168
+ import { IndexedDbKvRepository } from "@workglow/storage";
169
+ const browserStore = new IndexedDbKvRepository("my-app-storage");
170
+
171
+ // Node.js/Bun (using SQLite)
172
+ import { SqliteKvRepository } from "@workglow/storage";
173
+ // Pass a file path or a Database instance (see @workglow/sqlite)
174
+ const sqliteStore = new SqliteKvRepository("./data.db", "config_table");
175
+
176
+ // PostgreSQL (Node.js/Bun)
177
+ import { PostgresKvRepository } from "@workglow/storage";
178
+ import { Pool } from "pg";
179
+ const pool = new Pool({ connectionString: "postgresql://..." });
180
+ const pgStore = new PostgresKvRepository(pool, "settings");
181
+
182
+ // Supabase (Node.js/Bun)
183
+ import { SupabaseKvRepository } from "@workglow/storage";
184
+ import { createClient } from "@supabase/supabase-js";
185
+ const supabase = createClient("https://your-project.supabase.co", "your-anon-key");
186
+ const supabaseStore = new SupabaseKvRepository(supabase, "settings");
187
+ ```
188
+
189
+ #### Bulk Operations
190
+
191
+ ```typescript
192
+ const store = new InMemoryKvRepository<string, { name: string; score: number }>();
193
+
194
+ // Bulk insert
195
+ await store.putBulk([
196
+ { key: "player1", value: { name: "Alice", score: 100 } },
197
+ { key: "player2", value: { name: "Bob", score: 85 } },
198
+ ]);
199
+
200
+ // Get all data
201
+ const allPlayers = await store.getAll();
202
+ // Result: [{ key: "player1", value: { name: "Alice", score: 100 } }, ...]
203
+
204
+ // Get size
205
+ const count = await store.size(); // 2
206
+ ```
207
+
208
+ #### Event Handling
209
+
210
+ ```typescript
211
+ const store = new InMemoryKvRepository<string, any>();
212
+
213
+ // Listen to storage events
214
+ store.on("put", (key, value) => {
215
+ console.log(`Stored: ${key} = ${JSON.stringify(value)}`);
216
+ });
217
+
218
+ store.on("get", (key, value) => {
219
+ console.log(`Retrieved: ${key} = ${value ? "found" : "not found"}`);
220
+ });
221
+
222
+ await store.put("test", { data: "example" }); // Triggers 'put' event
223
+ await store.get("test"); // Triggers 'get' event
224
+ ```
225
+
226
+ ### Tabular Storage
227
+
228
+ Structured storage with schemas, primary keys, and indexing for complex data relationships.
229
+
230
+ #### Schema Definition
231
+
232
+ ```typescript
233
+ import { JsonSchema } from "@workglow/util";
234
+ import { InMemoryTabularRepository } from "@workglow/storage";
235
+
236
+ // Define your entity schema
237
+ const UserSchema = {
238
+ type: "object",
239
+ properties: {
240
+ id: { type: "string" },
241
+ email: { type: "string" },
242
+ name: { type: "string" },
243
+ age: { type: "number" },
244
+ department: { type: "string" },
245
+ createdAt: { type: "string" },
246
+ },
247
+ required: ["id", "email", "name", "age", "department", "createdAt"],
248
+ additionalProperties: false,
249
+ } as const satisfies JsonSchema;
250
+
251
+ // Create repository with primary key and indexes
252
+ const userRepo = new InMemoryTabularRepository<typeof UserSchema, ["id"]>(
253
+ UserSchema,
254
+ ["id"], // Primary key (can be compound: ["dept", "id"])
255
+ ["email", "department", ["department", "age"]] // Indexes for fast lookups
256
+ );
257
+ ```
258
+
259
+ #### CRUD Operations
260
+
261
+ ```typescript
262
+ // Create
263
+ await userRepo.put({
264
+ id: "user_123",
265
+ email: "alice@company.com",
266
+ name: "Alice Johnson",
267
+ age: 28,
268
+ department: "Engineering",
269
+ createdAt: new Date().toISOString(),
270
+ });
271
+
272
+ // Read by primary key
273
+ const user = await userRepo.get({ id: "user_123" });
274
+
275
+ // Update (put with same primary key)
276
+ await userRepo.put({
277
+ ...user!,
278
+ age: 29, // Birthday!
279
+ });
280
+
281
+ // Delete
282
+ await userRepo.delete({ id: "user_123" });
283
+ ```
284
+
285
+ #### Bulk Operations
286
+
287
+ ```typescript
288
+ // Bulk insert
289
+ await userRepo.putBulk([
290
+ {
291
+ id: "1",
292
+ email: "alice@co.com",
293
+ name: "Alice",
294
+ age: 28,
295
+ department: "Engineering",
296
+ createdAt: "2024-01-01",
297
+ },
298
+ {
299
+ id: "2",
300
+ email: "bob@co.com",
301
+ name: "Bob",
302
+ age: 32,
303
+ department: "Sales",
304
+ createdAt: "2024-01-02",
305
+ },
306
+ {
307
+ id: "3",
308
+ email: "carol@co.com",
309
+ name: "Carol",
310
+ age: 26,
311
+ department: "Engineering",
312
+ createdAt: "2024-01-03",
313
+ },
314
+ ]);
315
+
316
+ // Get all records
317
+ const allUsers = await userRepo.getAll();
318
+
319
+ // Get repository size
320
+ const userCount = await userRepo.size();
321
+ ```
322
+
323
+ #### Searching and Filtering
324
+
325
+ ```typescript
326
+ // Search by partial match (uses indexes when available)
327
+ const engineeringUsers = await userRepo.search({ department: "Engineering" });
328
+ const adultUsers = await userRepo.search({ age: 25 }); // Exact match
329
+
330
+ // Delete by search criteria
331
+ await userRepo.deleteSearch("department", "Sales", "=");
332
+ await userRepo.deleteSearch("age", 65, ">="); // Delete users 65 and older
333
+ ```
334
+
335
+ #### Environment-Specific Tabular Storage
336
+
337
+ ```typescript
338
+ // SQLite (Node.js/Bun)
339
+ import { SqliteTabularRepository } from "@workglow/storage";
340
+
341
+ const sqliteUsers = new SqliteTabularRepository<typeof UserSchema, ["id"]>(
342
+ "./users.db",
343
+ "users",
344
+ UserSchema,
345
+ ["id"],
346
+ ["email"]
347
+ );
348
+
349
+ // PostgreSQL (Node.js/Bun)
350
+ import { PostgresTabularRepository } from "@workglow/storage";
351
+ import { Pool } from "pg";
352
+
353
+ const pool = new Pool({ connectionString: "postgresql://..." });
354
+ const pgUsers = new PostgresTabularRepository<typeof UserSchema, ["id"]>(
355
+ pool,
356
+ "users",
357
+ UserSchema,
358
+ ["id"],
359
+ ["email"]
360
+ );
361
+
362
+ // Supabase (Node.js/Bun)
363
+ import { SupabaseTabularRepository } from "@workglow/storage";
364
+ import { createClient } from "@supabase/supabase-js";
365
+
366
+ const supabase = createClient("https://your-project.supabase.co", "your-anon-key");
367
+ const supabaseUsers = new SupabaseTabularRepository<typeof UserSchema, ["id"]>(
368
+ supabase,
369
+ "users",
370
+ UserSchema,
371
+ ["id"],
372
+ ["email"]
373
+ );
374
+
375
+ // IndexedDB (Browser)
376
+ import { IndexedDbTabularRepository } from "@workglow/storage";
377
+ const browserUsers = new IndexedDbTabularRepository<typeof UserSchema, ["id"]>(
378
+ "users",
379
+ UserSchema,
380
+ ["id"],
381
+ ["email"]
382
+ );
383
+
384
+ // File-based (Node.js/Bun)
385
+ import { FsFolderTabularRepository } from "@workglow/storage";
386
+ const fileUsers = new FsFolderTabularRepository<typeof UserSchema, ["id"]>(
387
+ "./data/users",
388
+ UserSchema,
389
+ ["id"],
390
+ ["email"]
391
+ );
392
+ ```
393
+
394
+ ### Queue Storage
395
+
396
+ Persistent job queue storage for background processing and task management.
397
+
398
+ > **Note**: Queue storage is primarily used internally by the job queue system. Direct usage is for advanced scenarios.
399
+
400
+ #### Basic Job Queue Operations
401
+
402
+ ```typescript
403
+ import { InMemoryQueueStorage, JobStatus } from "@workglow/storage";
404
+
405
+ // Define job input/output types
406
+ type ProcessingInput = { text: string; options: any };
407
+ type ProcessingOutput = { result: string; metadata: any };
408
+
409
+ const jobQueue = new InMemoryQueueStorage<ProcessingInput, ProcessingOutput>();
410
+
411
+ // Add job to queue
412
+ const jobId = await jobQueue.add({
413
+ input: { text: "Hello world", options: { uppercase: true } },
414
+ run_after: null, // Run immediately
415
+ max_retries: 3,
416
+ });
417
+
418
+ // Get next job for processing
419
+ const job = await jobQueue.next();
420
+ if (job) {
421
+ // Process the job...
422
+ const result = { result: "HELLO WORLD", metadata: { processed: true } };
423
+
424
+ // Mark as complete
425
+ await jobQueue.complete({
426
+ ...job,
427
+ output: result,
428
+ status: JobStatus.COMPLETED,
429
+ });
430
+ }
431
+ ```
432
+
433
+ #### Job Management
434
+
435
+ ```typescript
436
+ // Check queue status
437
+ const pendingCount = await jobQueue.size(JobStatus.PENDING);
438
+ const processingCount = await jobQueue.size(JobStatus.PROCESSING);
439
+
440
+ // Peek at jobs without removing them
441
+ const nextJobs = await jobQueue.peek(JobStatus.PENDING, 5);
442
+
443
+ // Progress tracking
444
+ await jobQueue.saveProgress(jobId, 50, "Processing...", { step: 1 });
445
+
446
+ // Handle job failures
447
+ await jobQueue.abort(jobId);
448
+
449
+ // Cleanup old completed jobs
450
+ await jobQueue.deleteJobsByStatusAndAge(JobStatus.COMPLETED, 24 * 60 * 60 * 1000); // 24 hours
451
+ ```
452
+
453
+ ## Environment-Specific Usage
454
+
455
+ ### Browser Environment
456
+
457
+ ```typescript
458
+ import {
459
+ IndexedDbKvRepository,
460
+ IndexedDbTabularRepository,
461
+ IndexedDbQueueStorage,
462
+ SupabaseKvRepository,
463
+ SupabaseTabularRepository,
464
+ SupabaseQueueStorage,
465
+ } from "@workglow/storage";
466
+ import { createClient } from "@supabase/supabase-js";
467
+
468
+ // Local browser storage with IndexedDB
469
+ const settings = new IndexedDbKvRepository("app-settings");
470
+ const userData = new IndexedDbTabularRepository("users", UserSchema, ["id"]);
471
+ const jobQueue = new IndexedDbQueueStorage<any, any>("background-jobs");
472
+
473
+ // Or use Supabase for cloud storage from the browser
474
+ const supabase = createClient("https://your-project.supabase.co", "your-anon-key");
475
+ const cloudSettings = new SupabaseKvRepository(supabase, "app-settings");
476
+ const cloudUserData = new SupabaseTabularRepository(supabase, "users", UserSchema, ["id"]);
477
+ const cloudJobQueue = new SupabaseQueueStorage(supabase, "background-jobs");
478
+ ```
479
+
480
+ ### Node.js Environment
481
+
482
+ ```typescript
483
+ import {
484
+ SqliteKvRepository,
485
+ PostgresTabularRepository,
486
+ FsFolderJsonKvRepository,
487
+ } from "@workglow/storage";
488
+
489
+ // Mix and match storage backends
490
+ const cache = new FsFolderJsonKvRepository("./cache");
491
+ const users = new PostgresTabularRepository(pool, "users", UserSchema, ["id"]);
492
+ ```
493
+
494
+ ### Bun Environment
495
+
496
+ ```typescript
497
+ // Bun has access to all implementations
498
+ import {
499
+ SqliteTabularRepository,
500
+ FsFolderJsonKvRepository,
501
+ PostgresQueueStorage,
502
+ SupabaseTabularRepository,
503
+ } from "@workglow/storage";
504
+
505
+ import { Database } from "bun:sqlite";
506
+ import { createClient } from "@supabase/supabase-js";
507
+
508
+ const db = new Database("./app.db");
509
+ const data = new SqliteTabularRepository(db, "items", ItemSchema, ["id"]);
510
+
511
+ // Or use Supabase for cloud storage
512
+ const supabase = createClient("https://your-project.supabase.co", "your-anon-key");
513
+ const cloudData = new SupabaseTabularRepository(supabase, "items", ItemSchema, ["id"]);
514
+ ```
515
+
516
+ ## Advanced Features
517
+
518
+ ### Event-Driven Architecture
519
+
520
+ All storage implementations support event emission for monitoring and reactive programming:
521
+
522
+ ```typescript
523
+ const store = new InMemoryTabularRepository(UserSchema, ["id"]);
524
+
525
+ // Monitor all operations
526
+ store.on("put", (entity) => console.log("User created/updated:", entity));
527
+ store.on("delete", (key) => console.log("User deleted:", key));
528
+ store.on("get", (key, entity) => console.log("User accessed:", entity ? "found" : "not found"));
529
+
530
+ // Wait for specific events
531
+ const [entity] = await store.waitOn("put"); // Waits for next put operation
532
+ ```
533
+
534
+ ### Compound Primary Keys
535
+
536
+ ```typescript
537
+ import { JsonSchema } from "@workglow/util";
538
+
539
+ const OrderLineSchema = {
540
+ type: "object",
541
+ properties: {
542
+ orderId: { type: "string" },
543
+ lineNumber: { type: "number" },
544
+ productId: { type: "string" },
545
+ quantity: { type: "number" },
546
+ price: { type: "number" },
547
+ },
548
+ required: ["orderId", "lineNumber", "productId", "quantity", "price"],
549
+ additionalProperties: false,
550
+ } as const satisfies JsonSchema;
551
+
552
+ const orderLines = new InMemoryTabularRepository<typeof OrderLineSchema, ["orderId", "lineNumber"]>(
553
+ OrderLineSchema,
554
+ ["orderId", "lineNumber"], // Compound primary key
555
+ ["productId"] // Additional index
556
+ );
557
+
558
+ // Use compound keys
559
+ await orderLines.put({
560
+ orderId: "ORD-123",
561
+ lineNumber: 1,
562
+ productId: "PROD-A",
563
+ quantity: 2,
564
+ price: 19.99,
565
+ });
566
+ const line = await orderLines.get({ orderId: "ORD-123", lineNumber: 1 });
567
+ ```
568
+
569
+ ### Custom File Layout (KV on filesystem)
570
+
571
+ ```typescript
572
+ import { FsFolderKvRepository } from "@workglow/storage";
573
+ import { JsonSchema } from "@workglow/util";
574
+
575
+ // Control how keys map to file paths and value encoding via schemas
576
+ const keySchema = { type: "string" } as const satisfies JsonSchema;
577
+ const valueSchema = { type: "string" } as const satisfies JsonSchema;
578
+
579
+ const files = new FsFolderKvRepository<string, string>(
580
+ "./data/files",
581
+ (key) => `${key}.txt`,
582
+ keySchema,
583
+ valueSchema
584
+ );
585
+
586
+ await files.put("note-1", "Hello world");
587
+ ```
588
+
589
+ ## API Reference
590
+
591
+ ### IKvRepository<Key, Value>
592
+
593
+ Core interface for key-value storage:
594
+
595
+ ```typescript
596
+ interface IKvRepository<Key, Value> {
597
+ // Core operations
598
+ put(key: Key, value: Value): Promise<void>;
599
+ putBulk(items: Array<{ key: Key; value: Value }>): Promise<void>;
600
+ get(key: Key): Promise<Value | undefined>;
601
+ delete(key: Key): Promise<void>;
602
+ getAll(): Promise<Array<{ key: Key; value: Value }> | undefined>;
603
+ deleteAll(): Promise<void>;
604
+ size(): Promise<number>;
605
+
606
+ // Event handling
607
+ on(event: "put" | "get" | "getAll" | "delete" | "deleteall", callback: Function): void;
608
+ off(event: string, callback: Function): void;
609
+ once(event: string, callback: Function): void;
610
+ waitOn(event: string): Promise<any[]>;
611
+ emit(event: string, ...args: any[]): void;
612
+ }
613
+ ```
614
+
615
+ ### ITabularRepository<Schema, PrimaryKeyNames>
616
+
617
+ Core interface for tabular storage:
618
+
619
+ ```typescript
620
+ interface ITabularRepository<Schema, PrimaryKeyNames, Entity, PrimaryKey, Value> {
621
+ // Core operations
622
+ put(entity: Entity): Promise<void>;
623
+ putBulk(entities: Entity[]): Promise<void>;
624
+ get(key: PrimaryKey): Promise<Entity | undefined>;
625
+ delete(key: PrimaryKey | Entity): Promise<void>;
626
+ getAll(): Promise<Entity[] | undefined>;
627
+ deleteAll(): Promise<void>;
628
+ size(): Promise<number>;
629
+
630
+ // Search operations
631
+ search(criteria: Partial<Entity>): Promise<Entity[] | undefined>;
632
+ deleteSearch(
633
+ column: keyof Entity,
634
+ value: any,
635
+ operator: "=" | "<" | "<=" | ">" | ">="
636
+ ): Promise<void>;
637
+
638
+ // Event handling
639
+ on(event: "put" | "get" | "search" | "delete" | "clearall", callback: Function): void;
640
+ off(event: string, callback: Function): void;
641
+ once(event: string, callback: Function): void;
642
+ waitOn(event: string): Promise<any[]>;
643
+ emit(event: string, ...args: any[]): void;
644
+ }
645
+ ```
646
+
647
+ ### IQueueStorage<Input, Output>
648
+
649
+ Core interface for job queue storage:
650
+
651
+ ```typescript
652
+ interface IQueueStorage<Input, Output> {
653
+ add(job: JobStorageFormat<Input, Output>): Promise<unknown>;
654
+ get(id: unknown): Promise<JobStorageFormat<Input, Output> | undefined>;
655
+ next(): Promise<JobStorageFormat<Input, Output> | undefined>;
656
+ complete(job: JobStorageFormat<Input, Output>): Promise<void>;
657
+ peek(status?: JobStatus, num?: number): Promise<JobStorageFormat<Input, Output>[]>;
658
+ size(status?: JobStatus): Promise<number>;
659
+ abort(id: unknown): Promise<void>;
660
+ saveProgress(id: unknown, progress: number, message: string, details: any): Promise<void>;
661
+ deleteAll(): Promise<void>;
662
+ getByRunId(runId: string): Promise<Array<JobStorageFormat<Input, Output>>>;
663
+ outputForInput(input: Input): Promise<Output | null>;
664
+ delete(id: unknown): Promise<void>;
665
+ deleteJobsByStatusAndAge(status: JobStatus, olderThanMs: number): Promise<void>;
666
+ }
667
+ ```
668
+
669
+ ## Examples
670
+
671
+ ### User Management System
672
+
673
+ ```typescript
674
+ import { JsonSchema, FromSchema } from "@workglow/util";
675
+ import { InMemoryTabularRepository, InMemoryKvRepository } from "@workglow/storage";
676
+
677
+ // User profile with tabular storage
678
+ const UserSchema = {
679
+ type: "object",
680
+ properties: {
681
+ id: { type: "string" },
682
+ username: { type: "string" },
683
+ email: { type: "string" },
684
+ firstName: { type: "string" },
685
+ lastName: { type: "string" },
686
+ role: {
687
+ type: "string",
688
+ enum: ["admin", "user", "guest"],
689
+ },
690
+ createdAt: { type: "string" },
691
+ lastLoginAt: { type: "string" },
692
+ },
693
+ required: ["id", "username", "email", "firstName", "lastName", "role", "createdAt"],
694
+ additionalProperties: false,
695
+ } as const satisfies JsonSchema;
696
+
697
+ const userRepo = new InMemoryTabularRepository<typeof UserSchema, ["id"]>(
698
+ UserSchema,
699
+ ["id"],
700
+ ["email", "username"]
701
+ );
702
+
703
+ // User sessions with KV storage
704
+ const sessionStore = new InMemoryKvRepository<string, { userId: string; expiresAt: string }>();
705
+
706
+ // User management class
707
+ class UserManager {
708
+ constructor(
709
+ private userRepo: typeof userRepo,
710
+ private sessionStore: typeof sessionStore
711
+ ) {}
712
+
713
+ async createUser(userData: Omit<FromSchema<typeof UserSchema>, "id" | "createdAt">) {
714
+ const user = {
715
+ ...userData,
716
+ id: crypto.randomUUID(),
717
+ createdAt: new Date().toISOString(),
718
+ };
719
+ await this.userRepo.put(user);
720
+ return user;
721
+ }
722
+
723
+ async loginUser(email: string): Promise<string> {
724
+ const users = await this.userRepo.search({ email });
725
+ if (!users?.length) throw new Error("User not found");
726
+
727
+ const sessionId = crypto.randomUUID();
728
+ await this.sessionStore.put(sessionId, {
729
+ userId: users[0].id,
730
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
731
+ });
732
+
733
+ // Update last login
734
+ await this.userRepo.put({
735
+ ...users[0],
736
+ lastLoginAt: new Date().toISOString(),
737
+ });
738
+
739
+ return sessionId;
740
+ }
741
+
742
+ async getSessionUser(sessionId: string) {
743
+ const session = await this.sessionStore.get(sessionId);
744
+ if (!session || new Date(session.expiresAt) < new Date()) {
745
+ return null;
746
+ }
747
+ return this.userRepo.get({ id: session.userId });
748
+ }
749
+ }
750
+ ```
751
+
752
+ ### Configuration Management
753
+
754
+ ```typescript
755
+ // Application settings with typed configuration
756
+ type AppConfig = {
757
+ database: {
758
+ host: string;
759
+ port: number;
760
+ name: string;
761
+ };
762
+ features: {
763
+ enableNewUI: boolean;
764
+ maxUploadSize: number;
765
+ };
766
+ integrations: {
767
+ stripe: { apiKey: string; webhook: string };
768
+ sendgrid: { apiKey: string };
769
+ };
770
+ };
771
+
772
+ const configStore = new FsFolderJsonKvRepository<string, AppConfig>("./config");
773
+
774
+ class ConfigManager {
775
+ private cache = new Map<string, AppConfig>();
776
+
777
+ constructor(private store: typeof configStore) {
778
+ // Listen for config changes
779
+ store.on("put", (key, value) => {
780
+ this.cache.set(key, value);
781
+ console.log(`Configuration updated: ${key}`);
782
+ });
783
+ }
784
+
785
+ async getConfig(environment: string): Promise<AppConfig> {
786
+ if (this.cache.has(environment)) {
787
+ return this.cache.get(environment)!;
788
+ }
789
+
790
+ const config = await this.store.get(environment);
791
+ if (!config) throw new Error(`No configuration for environment: ${environment}`);
792
+
793
+ this.cache.set(environment, config);
794
+ return config;
795
+ }
796
+
797
+ async updateConfig(environment: string, updates: Partial<AppConfig>) {
798
+ const current = await this.getConfig(environment);
799
+ const updated = { ...current, ...updates };
800
+ await this.store.put(environment, updated);
801
+ }
802
+ }
803
+ ```
804
+
805
+ ### Supabase Integration Example
806
+
807
+ ```typescript
808
+ import { createClient } from "@supabase/supabase-js";
809
+ import { JsonSchema } from "@workglow/util";
810
+ import {
811
+ SupabaseTabularRepository,
812
+ SupabaseKvRepository,
813
+ SupabaseQueueStorage,
814
+ } from "@workglow/storage";
815
+
816
+ // Initialize Supabase client
817
+ const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!);
818
+
819
+ // Define schemas
820
+ const ProductSchema = {
821
+ type: "object",
822
+ properties: {
823
+ id: { type: "string" },
824
+ name: { type: "string" },
825
+ price: { type: "number" },
826
+ category: { type: "string" },
827
+ stock: { type: "number", minimum: 0 },
828
+ createdAt: { type: "string", format: "date-time" },
829
+ },
830
+ required: ["id", "name", "price", "category", "stock", "createdAt"],
831
+ additionalProperties: false,
832
+ } as const satisfies JsonSchema;
833
+
834
+ const OrderSchema = {
835
+ type: "object",
836
+ properties: {
837
+ id: { type: "string" },
838
+ customerId: { type: "string" },
839
+ productId: { type: "string" },
840
+ quantity: { type: "number", minimum: 1 },
841
+ status: {
842
+ type: "string",
843
+ enum: ["pending", "processing", "completed", "cancelled"],
844
+ },
845
+ createdAt: { type: "string", format: "date-time" },
846
+ },
847
+ required: ["id", "customerId", "productId", "quantity", "status", "createdAt"],
848
+ additionalProperties: false,
849
+ } as const satisfies JsonSchema;
850
+
851
+ // Create repositories
852
+ const products = new SupabaseTabularRepository<typeof ProductSchema, ["id"]>(
853
+ supabase,
854
+ "products",
855
+ ProductSchema,
856
+ ["id"],
857
+ ["category", "name"] // Indexed columns for fast searching
858
+ );
859
+
860
+ const orders = new SupabaseTabularRepository<typeof OrderSchema, ["id"]>(
861
+ supabase,
862
+ "orders",
863
+ OrderSchema,
864
+ ["id"],
865
+ ["customerId", "status", ["customerId", "status"]] // Compound index
866
+ );
867
+
868
+ // Use KV for caching
869
+ const cache = new SupabaseKvRepository(supabase, "cache");
870
+
871
+ // Use queue for background processing
872
+ type EmailJob = { to: string; subject: string; body: string };
873
+ const emailQueue = new SupabaseQueueStorage<EmailJob, void>(supabase, "emails");
874
+
875
+ // Example usage
876
+ async function createOrder(customerId: string, productId: string, quantity: number) {
877
+ // Check product availability
878
+ const product = await products.get({ id: productId });
879
+ if (!product || product.stock < quantity) {
880
+ throw new Error("Insufficient stock");
881
+ }
882
+
883
+ // Create order
884
+ const order = {
885
+ id: crypto.randomUUID(),
886
+ customerId,
887
+ productId,
888
+ quantity,
889
+ status: "pending" as const,
890
+ createdAt: new Date().toISOString(),
891
+ };
892
+ await orders.put(order);
893
+
894
+ // Update stock
895
+ await products.put({
896
+ ...product,
897
+ stock: product.stock - quantity,
898
+ });
899
+
900
+ // Queue email notification
901
+ await emailQueue.add({
902
+ input: {
903
+ to: customerId,
904
+ subject: "Order Confirmation",
905
+ body: `Your order ${order.id} has been confirmed!`,
906
+ },
907
+ run_after: null,
908
+ max_retries: 3,
909
+ });
910
+
911
+ return order;
912
+ }
913
+
914
+ // Get customer's orders
915
+ async function getCustomerOrders(customerId: string) {
916
+ return await orders.search({ customerId });
917
+ }
918
+
919
+ // Get orders by status
920
+ async function getOrdersByStatus(status: string) {
921
+ return await orders.search({ status });
922
+ }
923
+ ```
924
+
925
+ **Important Note**
926
+ The implementations assume you have an exec_sql RPC function in your Supabase database for table creation, or that you've created the tables through Supabase migrations. For production use, it's recommended to:
927
+
928
+ - Create tables using Supabase migrations rather than runtime table creation
929
+ - Set up proper Row Level Security (RLS) policies in Supabase
930
+ - Use service role keys for server-side operations that need elevated permissions
931
+
932
+ ## Testing
933
+
934
+ The package includes comprehensive test suites for all storage implementations:
935
+
936
+ ```bash
937
+ # Run all tests
938
+ bun test
939
+
940
+ # Run specific test suites
941
+ bun test --grep "KvRepository"
942
+ bun test --grep "TabularRepository"
943
+ bun test --grep "QueueStorage"
944
+
945
+ # Test specific environments
946
+ bun test --grep "InMemory" # Cross-platform tests
947
+ bun test --grep "IndexedDb" # Browser tests
948
+ bun test --grep "Sqlite" # Native tests
949
+ ```
950
+
951
+ ### Writing Tests for Your Storage Usage
952
+
953
+ ```typescript
954
+ import { describe, test, expect, beforeEach } from "vitest";
955
+ import { InMemoryTabularRepository } from "@workglow/storage";
956
+
957
+ describe("UserRepository", () => {
958
+ let userRepo: InMemoryTabularRepository<typeof UserSchema, ["id"]>;
959
+
960
+ beforeEach(() => {
961
+ userRepo = new InMemoryTabularRepository<typeof UserSchema, ["id"]>(
962
+ UserSchema,
963
+ ["id"],
964
+ ["email"]
965
+ );
966
+ });
967
+
968
+ test("should create and retrieve user", async () => {
969
+ const user = {
970
+ id: "test-123",
971
+ email: "test@example.com",
972
+ name: "Test User",
973
+ age: 25,
974
+ department: "Engineering",
975
+ createdAt: new Date().toISOString(),
976
+ };
977
+
978
+ await userRepo.put(user);
979
+ const retrieved = await userRepo.get({ id: "test-123" });
980
+
981
+ expect(retrieved).toEqual(user);
982
+ });
983
+
984
+ test("should find users by department", async () => {
985
+ const users = [
986
+ {
987
+ id: "1",
988
+ email: "alice@co.com",
989
+ name: "Alice",
990
+ age: 28,
991
+ department: "Engineering",
992
+ createdAt: "2024-01-01",
993
+ },
994
+ {
995
+ id: "2",
996
+ email: "bob@co.com",
997
+ name: "Bob",
998
+ age: 32,
999
+ department: "Sales",
1000
+ createdAt: "2024-01-02",
1001
+ },
1002
+ ];
1003
+
1004
+ await userRepo.putBulk(users);
1005
+ const engineers = await userRepo.search({ department: "Engineering" });
1006
+
1007
+ expect(engineers).toHaveLength(1);
1008
+ expect(engineers![0].name).toBe("Alice");
1009
+ });
1010
+ });
1011
+ ```
1012
+
1013
+ ## License
1014
+
1015
+ Apache 2.0 - See [LICENSE](./LICENSE) for details