jotdb 0.1.3 โ 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -172
- package/bun.lock +1 -3
- package/dist/index.d.ts +6 -5
- package/dist/index.js +60 -57
- package/package.json +3 -6
- package/src/index.ts +120 -70
- package/jotdb.tests.ts +0 -63
package/README.md
CHANGED
|
@@ -1,203 +1,78 @@
|
|
|
1
1
|
# JotDB
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight, schema-less database built on Cloudflare Durable Objects. Perfect for quick prototyping and applications that need simple data storage without the complexity of traditional databases.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why JotDB?
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
bun add jotdb
|
|
9
|
-
# or
|
|
10
|
-
npm install jotdb
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
### 2. **Bind the Durable Object in your wrangler.toml or wrangler.json**
|
|
14
|
-
|
|
15
|
-
```toml
|
|
16
|
-
[[durable_objects.bindings]]
|
|
17
|
-
name = "JOTDB"
|
|
18
|
-
class_name = "JotDB"
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### 3. **Register the Durable Object in your Worker**
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
import { JotDB } from 'jotdb';
|
|
25
|
-
|
|
26
|
-
export interface Env {
|
|
27
|
-
JOTDB: DurableObjectNamespace<JotDB>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default {
|
|
31
|
-
async fetch(request: Request, env: Env) {
|
|
32
|
-
// Get a stub for your JotDB instance
|
|
33
|
-
const id = env.JOTDB.idFromName("my-db");
|
|
34
|
-
const db = env.JOTDB.get(id);
|
|
35
|
-
|
|
36
|
-
// Use RPC (recommended, requires extends DurableObject)
|
|
37
|
-
await db.set("key", "value");
|
|
38
|
-
const value = await db.get("key");
|
|
39
|
-
|
|
40
|
-
return new Response(`Value: ${value}`);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 4. **Deploy or run locally**
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
wrangler dev
|
|
49
|
-
# or
|
|
50
|
-
wrangler deploy
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## ๐ Notes
|
|
56
|
-
|
|
57
|
-
- **RPC support:** JotDB uses Cloudflare's new JavaScript-native RPC. You can call methods directly on the stub (e.g., `db.set(...)`, `db.get(...)`).
|
|
58
|
-
- **No fetch needed:** You do not need to use HTTP fetch to communicate with your Durable Objectโjust call methods!
|
|
59
|
-
- **TypeScript:** Use `DurableObjectNamespace<JotDB>` for full type safety.
|
|
60
|
-
- **See the API section below for all available methods.**
|
|
61
|
-
|
|
62
|
-
---
|
|
7
|
+
I needed a quick way to save data without dealing with schemas, SQL, or complex database setup. While Firestore is great, it can be overkill for simple use cases. JotDB provides a simpler alternative by leveraging Cloudflare Durable Objects, making it perfect for:
|
|
63
8
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
export interface Env {
|
|
70
|
-
JOTDB: DurableObjectNamespace<JotDB>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default {
|
|
74
|
-
async fetch(request: Request, env: Env) {
|
|
75
|
-
const id = env.JOTDB.idFromName("my-db");
|
|
76
|
-
const db = env.JOTDB.get(id);
|
|
77
|
-
|
|
78
|
-
await db.setSchema({
|
|
79
|
-
name: "string",
|
|
80
|
-
age: "number",
|
|
81
|
-
email: "email"
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
await db.setAll({
|
|
85
|
-
name: "Alice",
|
|
86
|
-
age: 42,
|
|
87
|
-
email: "alice@example.com"
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const all = await db.getAll();
|
|
91
|
-
|
|
92
|
-
return new Response(JSON.stringify(all, null, 2), {
|
|
93
|
-
headers: { "Content-Type": "application/json" }
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
A lightweight, schema-validated key-value store built on Cloudflare Durable Objects.
|
|
102
|
-
|
|
103
|
-
## Features
|
|
104
|
-
|
|
105
|
-
- Schema validation using Zod
|
|
106
|
-
- Automatic schema inference
|
|
107
|
-
- Audit logging
|
|
108
|
-
- TypeScript support
|
|
109
|
-
- Read-only mode
|
|
110
|
-
- Auto-strip mode for schema validation
|
|
9
|
+
- Quick prototypes
|
|
10
|
+
- Small to medium applications
|
|
11
|
+
- Serverless environments
|
|
12
|
+
- Real-time data storage
|
|
13
|
+
- Collaborative applications
|
|
111
14
|
|
|
112
15
|
## Installation
|
|
113
16
|
|
|
114
17
|
```bash
|
|
18
|
+
# Using npm
|
|
115
19
|
npm install jotdb
|
|
116
|
-
|
|
117
|
-
|
|
20
|
+
|
|
21
|
+
# Using yarn
|
|
22
|
+
yarn add jotdb
|
|
23
|
+
|
|
24
|
+
# Using pnpm
|
|
25
|
+
pnpm add jotdb
|
|
118
26
|
```
|
|
119
27
|
|
|
120
|
-
##
|
|
28
|
+
## Full Example
|
|
121
29
|
|
|
122
30
|
```typescript
|
|
123
31
|
import { JotDB } from 'jotdb';
|
|
124
32
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export default {
|
|
131
|
-
async fetch(request: Request, env: Env) {
|
|
132
|
-
const id = env.JOTDB.idFromName("my-db");
|
|
133
|
-
const db = env.JOTDB.get(id);
|
|
134
|
-
|
|
135
|
-
// Set a value
|
|
136
|
-
await db.set("key", "value");
|
|
137
|
-
|
|
138
|
-
// Get a value
|
|
139
|
-
const value = await db.get("key");
|
|
140
|
-
|
|
141
|
-
// Set schema
|
|
142
|
-
await db.setSchema({
|
|
143
|
-
name: "string",
|
|
144
|
-
age: "number",
|
|
145
|
-
email: "email"
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Set multiple values
|
|
149
|
-
await db.setAll({
|
|
150
|
-
name: "John",
|
|
151
|
-
age: 30,
|
|
152
|
-
email: "john@example.com"
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
```
|
|
33
|
+
// Initialize the database
|
|
34
|
+
const jotId = env.JotDB.idFromName("my-db");
|
|
35
|
+
const db = env.JotDB.get(jotId);
|
|
157
36
|
|
|
158
|
-
|
|
37
|
+
// Set a value
|
|
38
|
+
await db.set("user:123", { name: "John", age: 30 });
|
|
159
39
|
|
|
160
|
-
|
|
40
|
+
// Get a value
|
|
41
|
+
const user = await db.get("user:123");
|
|
42
|
+
console.log(user); // { name: "John", age: 30 }
|
|
161
43
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- `setAll(obj: Record<string, unknown>): Promise<void>`
|
|
166
|
-
- `delete(key: string): Promise<void>`
|
|
167
|
-
- `clear(): Promise<void>`
|
|
168
|
-
- `keys(): Promise<string[]>`
|
|
169
|
-
- `has(key: string): Promise<boolean>`
|
|
170
|
-
- `getSchema(): Promise<SchemaDefinition>`
|
|
171
|
-
- `setSchema(schema: SchemaDefinition): Promise<void>`
|
|
172
|
-
- `getOptions(): Promise<JotDBOptions>`
|
|
173
|
-
- `setOptions(opts: Partial<JotDBOptions>): Promise<void>`
|
|
174
|
-
- `getAuditLog(): Promise<AuditLogEntry[]>`
|
|
175
|
-
- `clearAuditLog(): Promise<void>`
|
|
44
|
+
// Delete a value
|
|
45
|
+
await db.delete("user:123");
|
|
46
|
+
```
|
|
176
47
|
|
|
177
|
-
|
|
48
|
+
## API Reference
|
|
178
49
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
50
|
+
| Method | Description | Parameters | Returns |
|
|
51
|
+
|--------|-------------|------------|---------|
|
|
52
|
+
| `set(key, value)` | Store a value | `key: string`, `value: any` | `Promise<void>` |
|
|
53
|
+
| `get(key)` | Retrieve a value | `key: string` | `Promise<any>` |
|
|
54
|
+
| `delete(key)` | Remove a value | `key: string` | `Promise<void>` |
|
|
55
|
+
| `list(prefix?)` | List all keys (optionally filtered by prefix) | `prefix?: string` | `Promise<string[]>` |
|
|
182
56
|
|
|
183
|
-
|
|
184
|
-
autoStrip: boolean;
|
|
185
|
-
readOnly: boolean;
|
|
186
|
-
}
|
|
57
|
+
## Types
|
|
187
58
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
59
|
+
```typescript
|
|
60
|
+
interface JotDB {
|
|
61
|
+
set(key: string, value: any): Promise<void>;
|
|
62
|
+
get(key: string): Promise<any>;
|
|
63
|
+
delete(key: string): Promise<void>;
|
|
64
|
+
list(prefix?: string): Promise<string[]>;
|
|
192
65
|
}
|
|
193
66
|
```
|
|
194
67
|
|
|
195
68
|
## License
|
|
196
69
|
|
|
197
|
-
MIT
|
|
70
|
+
MIT License - feel free to use this in your own projects!
|
|
198
71
|
|
|
199
72
|
## Contributing
|
|
200
73
|
|
|
74
|
+
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
75
|
+
|
|
201
76
|
1. Fork the repository
|
|
202
77
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
203
78
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
@@ -206,6 +81,8 @@ MIT
|
|
|
206
81
|
|
|
207
82
|
## Testing
|
|
208
83
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
84
|
+
Currently, testing is done manually in production. We're working on adding a comprehensive test suite. For now, you can test the functionality by:
|
|
85
|
+
|
|
86
|
+
1. Deploying to Cloudflare Workers
|
|
87
|
+
2. Using the example endpoints
|
|
88
|
+
3. Verifying data persistence
|
package/bun.lock
CHANGED
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
"workspaces": {
|
|
4
4
|
"": {
|
|
5
5
|
"name": "jotdb",
|
|
6
|
-
"dependencies": {
|
|
7
|
-
"typescript": "^5.8.3",
|
|
8
|
-
},
|
|
9
6
|
"devDependencies": {
|
|
10
7
|
"@cloudflare/workers-types": "^4.20250517.0",
|
|
11
8
|
"hono": "^4.7.10",
|
|
9
|
+
"typescript": "^5.8.3",
|
|
12
10
|
"wrangler": "^4.15.2",
|
|
13
11
|
"zod": "^3.24.4",
|
|
14
12
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DurableObjectState } from "@cloudflare/workers-types";
|
|
2
1
|
import { Hono } from 'hono';
|
|
3
2
|
import { DurableObject } from "cloudflare:workers";
|
|
4
3
|
type SchemaType = "string" | "number" | "boolean" | "email" | "array" | "object" | "any";
|
|
@@ -18,14 +17,15 @@ export declare class JotDB extends DurableObject {
|
|
|
18
17
|
private zodSchema;
|
|
19
18
|
private options;
|
|
20
19
|
private auditLog;
|
|
21
|
-
constructor(state:
|
|
20
|
+
constructor(state: any, env: Env);
|
|
22
21
|
load(): Promise<void>;
|
|
23
22
|
save(): Promise<void>;
|
|
23
|
+
isArrayMode(): boolean;
|
|
24
|
+
push(item: unknown): Promise<void>;
|
|
25
|
+
setAll(objOrArr: Record<string, unknown> | unknown[]): Promise<void>;
|
|
26
|
+
getAll(): Promise<unknown>;
|
|
24
27
|
logAudit(action: string, keys: string[] | string): Promise<void>;
|
|
25
28
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
26
|
-
getAll(): Promise<Record<string, unknown>>;
|
|
27
|
-
set<T>(key: string, value: T): Promise<void>;
|
|
28
|
-
setAll(obj: Record<string, unknown>): Promise<void>;
|
|
29
29
|
delete(key: string): Promise<void>;
|
|
30
30
|
clear(): Promise<void>;
|
|
31
31
|
keys(): Promise<string[]>;
|
|
@@ -39,6 +39,7 @@ export declare class JotDB extends DurableObject {
|
|
|
39
39
|
clearAuditLog(): Promise<void>;
|
|
40
40
|
private buildZodSchema;
|
|
41
41
|
fetch(request: Request): Promise<Response>;
|
|
42
|
+
set(key: string, value: unknown): Promise<void>;
|
|
42
43
|
}
|
|
43
44
|
export interface Env {
|
|
44
45
|
JOTDB: DurableObjectNamespace;
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,8 @@ export class JotDB extends DurableObject {
|
|
|
16
16
|
this.auditLog = [];
|
|
17
17
|
}
|
|
18
18
|
async load() {
|
|
19
|
-
if (Object.keys(this.data).length === 0) {
|
|
20
|
-
this.data = (await this.ctx.storage.get("data"))
|
|
19
|
+
if (this.data == null || (typeof this.data === 'object' && Object.keys(this.data).length === 0)) {
|
|
20
|
+
this.data = (await this.ctx.storage.get("data")) ?? {};
|
|
21
21
|
}
|
|
22
22
|
if (Object.keys(this.rawSchema).length === 0) {
|
|
23
23
|
this.rawSchema = (await this.ctx.storage.get("__schema__")) || {};
|
|
@@ -33,6 +33,31 @@ export class JotDB extends DurableObject {
|
|
|
33
33
|
async save() {
|
|
34
34
|
await this.ctx.storage.put("data", this.data);
|
|
35
35
|
}
|
|
36
|
+
isArrayMode() {
|
|
37
|
+
return Array.isArray(this.data);
|
|
38
|
+
}
|
|
39
|
+
async push(item) {
|
|
40
|
+
await this.load();
|
|
41
|
+
if (!Array.isArray(this.data)) {
|
|
42
|
+
this.data = [];
|
|
43
|
+
}
|
|
44
|
+
this.data.push(item);
|
|
45
|
+
await this.save();
|
|
46
|
+
await this.logAudit("push", []);
|
|
47
|
+
}
|
|
48
|
+
async setAll(objOrArr) {
|
|
49
|
+
await this.load();
|
|
50
|
+
if (!Array.isArray(objOrArr) && this.zodSchema) {
|
|
51
|
+
objOrArr = this.zodSchema.parse(objOrArr);
|
|
52
|
+
}
|
|
53
|
+
this.data = objOrArr;
|
|
54
|
+
await this.save();
|
|
55
|
+
await this.logAudit("setAll", Array.isArray(objOrArr) ? [] : Object.keys(objOrArr));
|
|
56
|
+
}
|
|
57
|
+
async getAll() {
|
|
58
|
+
await this.load();
|
|
59
|
+
return this.data;
|
|
60
|
+
}
|
|
36
61
|
async logAudit(action, keys) {
|
|
37
62
|
const entry = {
|
|
38
63
|
timestamp: Date.now(),
|
|
@@ -44,63 +69,18 @@ export class JotDB extends DurableObject {
|
|
|
44
69
|
}
|
|
45
70
|
async get(key) {
|
|
46
71
|
await this.load();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
async getAll() {
|
|
50
|
-
await this.load();
|
|
51
|
-
return this.data;
|
|
52
|
-
}
|
|
53
|
-
async set(key, value) {
|
|
54
|
-
await this.load();
|
|
55
|
-
if (this.options.readOnly)
|
|
56
|
-
throw new Error("JotDB is in read-only mode");
|
|
57
|
-
if (this.zodSchema) {
|
|
58
|
-
const partialSchema = this.zodSchema.pick({ [key]: true });
|
|
59
|
-
partialSchema.parse({ [key]: value });
|
|
60
|
-
}
|
|
61
|
-
this.data[key] = value;
|
|
62
|
-
await this.save();
|
|
63
|
-
await this.logAudit("set", key);
|
|
64
|
-
}
|
|
65
|
-
async setAll(obj) {
|
|
66
|
-
await this.load();
|
|
67
|
-
if (this.options.readOnly)
|
|
68
|
-
throw new Error("JotDB is in read-only mode");
|
|
69
|
-
const typeOfValue = (v) => {
|
|
70
|
-
if (Array.isArray(v))
|
|
71
|
-
return "array";
|
|
72
|
-
switch (typeof v) {
|
|
73
|
-
case "string": return "string";
|
|
74
|
-
case "number": return "number";
|
|
75
|
-
case "boolean": return "boolean";
|
|
76
|
-
case "object": return "object";
|
|
77
|
-
default: return "any";
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
if (!this.zodSchema) {
|
|
81
|
-
const inferred = {};
|
|
82
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
83
|
-
inferred[k] = typeOfValue(v);
|
|
84
|
-
}
|
|
85
|
-
await this.setSchema(inferred);
|
|
72
|
+
if (!Array.isArray(this.data)) {
|
|
73
|
+
return this.data[key];
|
|
86
74
|
}
|
|
87
|
-
|
|
88
|
-
if (this.options.autoStrip) {
|
|
89
|
-
obj = this.zodSchema.parse(obj); // returns stripped
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
this.zodSchema.parse(obj); // strict match
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
Object.assign(this.data, obj);
|
|
96
|
-
await this.save();
|
|
97
|
-
await this.logAudit("setAll", Object.keys(obj));
|
|
75
|
+
return undefined;
|
|
98
76
|
}
|
|
99
77
|
async delete(key) {
|
|
100
78
|
await this.load();
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
79
|
+
if (!Array.isArray(this.data)) {
|
|
80
|
+
delete this.data[key];
|
|
81
|
+
await this.save();
|
|
82
|
+
await this.logAudit("delete", key);
|
|
83
|
+
}
|
|
104
84
|
}
|
|
105
85
|
async clear() {
|
|
106
86
|
this.data = {};
|
|
@@ -109,11 +89,17 @@ export class JotDB extends DurableObject {
|
|
|
109
89
|
}
|
|
110
90
|
async keys() {
|
|
111
91
|
await this.load();
|
|
112
|
-
|
|
92
|
+
if (!Array.isArray(this.data)) {
|
|
93
|
+
return Object.keys(this.data);
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
113
96
|
}
|
|
114
97
|
async has(key) {
|
|
115
98
|
await this.load();
|
|
116
|
-
|
|
99
|
+
if (!Array.isArray(this.data)) {
|
|
100
|
+
return key in this.data;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
117
103
|
}
|
|
118
104
|
async getSchema() {
|
|
119
105
|
await this.load();
|
|
@@ -191,6 +177,23 @@ export class JotDB extends DurableObject {
|
|
|
191
177
|
async fetch(request) {
|
|
192
178
|
return new Response("Hello, World!");
|
|
193
179
|
}
|
|
180
|
+
async set(key, value) {
|
|
181
|
+
await this.load();
|
|
182
|
+
if (this.options.readOnly) {
|
|
183
|
+
throw new Error("Database is in read-only mode");
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(this.data)) {
|
|
186
|
+
this.data[key] = value;
|
|
187
|
+
if (this.zodSchema) {
|
|
188
|
+
this.zodSchema.parse(this.data);
|
|
189
|
+
}
|
|
190
|
+
await this.save();
|
|
191
|
+
await this.logAudit("set", key);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw new Error("Cannot use set() in array mode");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
194
197
|
}
|
|
195
198
|
const app = new Hono();
|
|
196
199
|
// Middleware
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jotdb",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -11,15 +11,12 @@
|
|
|
11
11
|
},
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
13
|
"scripts": {
|
|
14
|
-
"dev": "wrangler dev --port 5173
|
|
15
|
-
"test": "bun test jotdb.tests.ts",
|
|
14
|
+
"dev": "wrangler dev --port 5173",
|
|
16
15
|
"deploy": "wrangler deploy",
|
|
17
16
|
"patch": "npm version patch && npm publish --access public"
|
|
18
17
|
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"typescript": "^5.8.3"
|
|
21
|
-
},
|
|
22
18
|
"devDependencies": {
|
|
19
|
+
"typescript": "^5.8.3",
|
|
23
20
|
"@cloudflare/workers-types": "^4.20250517.0",
|
|
24
21
|
"hono": "^4.7.10",
|
|
25
22
|
"wrangler": "^4.15.2",
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface AuditLogEntry {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export class JotDB extends DurableObject {
|
|
23
|
-
private data: Record<string, unknown> = {};
|
|
23
|
+
private data: Record<string, unknown> | unknown[] = {};
|
|
24
24
|
private rawSchema: SchemaDefinition = {};
|
|
25
25
|
private zodSchema: ZodObject<any> | null = null;
|
|
26
26
|
private options: JotDBOptions = {
|
|
@@ -34,8 +34,8 @@ export class JotDB extends DurableObject {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async load(): Promise<void> {
|
|
37
|
-
if (Object.keys(this.data).length === 0) {
|
|
38
|
-
this.data = (await this.ctx.storage.get("data"))
|
|
37
|
+
if (this.data == null || (typeof this.data === 'object' && Object.keys(this.data).length === 0)) {
|
|
38
|
+
this.data = (await this.ctx.storage.get("data")) ?? {};
|
|
39
39
|
}
|
|
40
40
|
if (Object.keys(this.rawSchema).length === 0) {
|
|
41
41
|
this.rawSchema = (await this.ctx.storage.get("__schema__")) || {};
|
|
@@ -45,7 +45,6 @@ export class JotDB extends DurableObject {
|
|
|
45
45
|
}
|
|
46
46
|
const storedOptions = await this.ctx.storage.get("__options__") as JotDBOptions | null;
|
|
47
47
|
if (storedOptions) this.options = storedOptions;
|
|
48
|
-
|
|
49
48
|
this.auditLog = (await this.ctx.storage.get("__audit__")) || [];
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -53,80 +52,60 @@ export class JotDB extends DurableObject {
|
|
|
53
52
|
await this.ctx.storage.put("data", this.data);
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
timestamp: Date.now(),
|
|
59
|
-
action,
|
|
60
|
-
keys: Array.isArray(keys) ? keys : [keys],
|
|
61
|
-
};
|
|
62
|
-
this.auditLog.unshift(entry);
|
|
63
|
-
await this.ctx.storage.put("__audit__", this.auditLog.slice(0, 100)); // keep max 100 entries
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
67
|
-
await this.load();
|
|
68
|
-
return this.data[key] as T;
|
|
55
|
+
isArrayMode(): boolean {
|
|
56
|
+
return Array.isArray(this.data);
|
|
69
57
|
}
|
|
70
58
|
|
|
71
|
-
async
|
|
59
|
+
async push(item: unknown): Promise<void> {
|
|
72
60
|
await this.load();
|
|
73
|
-
|
|
61
|
+
if (!Array.isArray(this.data)) {
|
|
62
|
+
this.data = [];
|
|
63
|
+
}
|
|
64
|
+
(this.data as unknown[]).push(item);
|
|
65
|
+
await this.save();
|
|
66
|
+
await this.logAudit("push", []);
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
async
|
|
69
|
+
async setAll(objOrArr: Record<string, unknown> | unknown[]): Promise<void> {
|
|
77
70
|
await this.load();
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
if (this.zodSchema) {
|
|
81
|
-
const partialSchema = this.zodSchema.pick({ [key]: true });
|
|
82
|
-
partialSchema.parse({ [key]: value });
|
|
71
|
+
if (!Array.isArray(objOrArr) && this.zodSchema) {
|
|
72
|
+
objOrArr = this.zodSchema.parse(objOrArr);
|
|
83
73
|
}
|
|
84
|
-
this.data
|
|
74
|
+
this.data = objOrArr;
|
|
85
75
|
await this.save();
|
|
86
|
-
await this.logAudit("
|
|
76
|
+
await this.logAudit("setAll", Array.isArray(objOrArr) ? [] : Object.keys(objOrArr));
|
|
87
77
|
}
|
|
88
78
|
|
|
89
|
-
async
|
|
79
|
+
async getAll(): Promise<unknown> {
|
|
90
80
|
await this.load();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const typeOfValue = (v: any): string => {
|
|
94
|
-
if (Array.isArray(v)) return "array";
|
|
95
|
-
switch (typeof v) {
|
|
96
|
-
case "string": return "string";
|
|
97
|
-
case "number": return "number";
|
|
98
|
-
case "boolean": return "boolean";
|
|
99
|
-
case "object": return "object";
|
|
100
|
-
default: return "any";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
81
|
+
return this.data;
|
|
82
|
+
}
|
|
103
83
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
84
|
+
async logAudit(action: string, keys: string[] | string): Promise<void> {
|
|
85
|
+
const entry: AuditLogEntry = {
|
|
86
|
+
timestamp: Date.now(),
|
|
87
|
+
action,
|
|
88
|
+
keys: Array.isArray(keys) ? keys : [keys],
|
|
89
|
+
};
|
|
90
|
+
this.auditLog.unshift(entry);
|
|
91
|
+
await this.ctx.storage.put("__audit__", this.auditLog.slice(0, 100)); // keep max 100 entries
|
|
92
|
+
}
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.zodSchema.parse(obj); // strict match
|
|
117
|
-
}
|
|
94
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
95
|
+
await this.load();
|
|
96
|
+
if (!Array.isArray(this.data)) {
|
|
97
|
+
return this.data[key] as T;
|
|
118
98
|
}
|
|
119
|
-
|
|
120
|
-
Object.assign(this.data, obj);
|
|
121
|
-
await this.save();
|
|
122
|
-
await this.logAudit("setAll", Object.keys(obj));
|
|
99
|
+
return undefined;
|
|
123
100
|
}
|
|
124
101
|
|
|
125
102
|
async delete(key: string): Promise<void> {
|
|
126
103
|
await this.load();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
104
|
+
if (!Array.isArray(this.data)) {
|
|
105
|
+
delete this.data[key];
|
|
106
|
+
await this.save();
|
|
107
|
+
await this.logAudit("delete", key);
|
|
108
|
+
}
|
|
130
109
|
}
|
|
131
110
|
|
|
132
111
|
async clear(): Promise<void> {
|
|
@@ -137,12 +116,18 @@ export class JotDB extends DurableObject {
|
|
|
137
116
|
|
|
138
117
|
async keys(): Promise<string[]> {
|
|
139
118
|
await this.load();
|
|
140
|
-
|
|
119
|
+
if (!Array.isArray(this.data)) {
|
|
120
|
+
return Object.keys(this.data);
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
141
123
|
}
|
|
142
124
|
|
|
143
125
|
async has(key: string): Promise<boolean> {
|
|
144
126
|
await this.load();
|
|
145
|
-
|
|
127
|
+
if (!Array.isArray(this.data)) {
|
|
128
|
+
return key in this.data;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
146
131
|
}
|
|
147
132
|
|
|
148
133
|
async getSchema(): Promise<SchemaDefinition> {
|
|
@@ -230,6 +215,23 @@ export class JotDB extends DurableObject {
|
|
|
230
215
|
async fetch(request: Request) {
|
|
231
216
|
return new Response("Hello, World!");
|
|
232
217
|
}
|
|
218
|
+
|
|
219
|
+
async set(key: string, value: unknown): Promise<void> {
|
|
220
|
+
await this.load();
|
|
221
|
+
if (this.options.readOnly) {
|
|
222
|
+
throw new Error("Database is in read-only mode");
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(this.data)) {
|
|
225
|
+
this.data[key] = value;
|
|
226
|
+
if (this.zodSchema) {
|
|
227
|
+
this.zodSchema.parse(this.data);
|
|
228
|
+
}
|
|
229
|
+
await this.save();
|
|
230
|
+
await this.logAudit("set", key);
|
|
231
|
+
} else {
|
|
232
|
+
throw new Error("Cannot use set() in array mode");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
export interface Env {
|
|
@@ -274,7 +276,7 @@ app.get('/test', async (c) => {
|
|
|
274
276
|
age: 30,
|
|
275
277
|
email: "john@example.com"
|
|
276
278
|
});
|
|
277
|
-
const all = await db.getAll();
|
|
279
|
+
const all = await db.getAll() as { name: string, age: number, email: string };
|
|
278
280
|
results.tests.push({
|
|
279
281
|
name: "Schema validation",
|
|
280
282
|
passed: all.name === "John" && all.age === 30,
|
|
@@ -306,23 +308,71 @@ app.get('/test', async (c) => {
|
|
|
306
308
|
email: "jane@example.com",
|
|
307
309
|
extra: "should be stripped"
|
|
308
310
|
});
|
|
309
|
-
const stripped = await db.getAll();
|
|
311
|
+
const stripped = await db.getAll() as { name: string, age: number, email: string };
|
|
310
312
|
results.tests.push({
|
|
311
313
|
name: "Auto-strip mode",
|
|
312
314
|
passed: !("extra" in stripped),
|
|
313
315
|
value: stripped
|
|
314
316
|
});
|
|
315
317
|
|
|
318
|
+
// Test 5: Array mode - setAll and getAll
|
|
319
|
+
const arrayId = c.env.JOTDB.idFromName("test-array");
|
|
320
|
+
const arrayDb = c.env.JOTDB.get(arrayId) as unknown as JotDB;
|
|
321
|
+
await arrayDb.setAll([1, 2, 3]);
|
|
322
|
+
const arr = await arrayDb.getAll();
|
|
323
|
+
results.tests.push({
|
|
324
|
+
name: "Array mode setAll/getAll",
|
|
325
|
+
passed: Array.isArray(arr) && arr.length === 3 && arr[0] === 1 && arr[2] === 3,
|
|
326
|
+
value: arr
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Test 6: Array mode - push
|
|
330
|
+
await arrayDb.push(4);
|
|
331
|
+
const arr2 = await arrayDb.getAll();
|
|
332
|
+
results.tests.push({
|
|
333
|
+
name: "Array mode push",
|
|
334
|
+
passed: Array.isArray(arr2) && arr2.length === 4 && arr2[3] === 4,
|
|
335
|
+
value: arr2
|
|
336
|
+
});
|
|
337
|
+
|
|
316
338
|
// Get audit log
|
|
317
339
|
results.auditLog = await db.getAuditLog();
|
|
318
340
|
|
|
319
|
-
|
|
341
|
+
// HTML output
|
|
342
|
+
let html = `<!DOCTYPE html><html><head><title>JotDB Test Results</title>
|
|
343
|
+
<style>
|
|
344
|
+
body { font-family: sans-serif; margin: 2em; }
|
|
345
|
+
.pass { color: green; }
|
|
346
|
+
.fail { color: red; }
|
|
347
|
+
.test { margin-bottom: 1em; }
|
|
348
|
+
pre { background: #f4f4f4; padding: 0.5em; }
|
|
349
|
+
</style>
|
|
350
|
+
</head><body>
|
|
351
|
+
<h1>JotDB Test Results</h1>
|
|
352
|
+
<p><b>Timestamp:</b> ${new Date(results.timestamp).toLocaleString()}</p>
|
|
353
|
+
<div>
|
|
354
|
+
${results.tests.map(test => `
|
|
355
|
+
<div class="test">
|
|
356
|
+
<b>${test.name}:</b> <span class="${test.passed ? 'pass' : 'fail'}">${test.passed ? 'PASS' : 'FAIL'}</span><br/>
|
|
357
|
+
<pre>${JSON.stringify(test.value ?? test.error, null, 2)}</pre>
|
|
358
|
+
</div>
|
|
359
|
+
`).join('')}
|
|
360
|
+
</div>
|
|
361
|
+
<h2>Audit Log</h2>
|
|
362
|
+
<pre>${JSON.stringify(results.auditLog, null, 2)}</pre>
|
|
363
|
+
</body></html>`;
|
|
364
|
+
|
|
365
|
+
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
|
|
320
366
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
367
|
+
let html = `<!DOCTYPE html><html><head><title>JotDB Test Error</title></head><body>` +
|
|
368
|
+
`<h1 style="color:red">Error</h1>` +
|
|
369
|
+
`<pre>${error instanceof Error ? error.message : String(error)}</pre>` +
|
|
370
|
+
`<h2>Partial Results</h2>` +
|
|
371
|
+
`<pre>${JSON.stringify(results.tests, null, 2)}</pre>` +
|
|
372
|
+
`<h2>Audit Log</h2>` +
|
|
373
|
+
`<pre>${JSON.stringify(results.auditLog, null, 2)}</pre>` +
|
|
374
|
+
`</body></html>`;
|
|
375
|
+
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
|
|
326
376
|
}
|
|
327
377
|
});
|
|
328
378
|
|
package/jotdb.tests.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { assert, describe, it, beforeEach } from "bun:test";
|
|
2
|
-
import { JotDB } from "./src/index.ts";
|
|
3
|
-
|
|
4
|
-
// Fake DurableObjectState stub for unit testing
|
|
5
|
-
function createFakeState(): any {
|
|
6
|
-
let store: Record<string, any> = {};
|
|
7
|
-
return {
|
|
8
|
-
storage: {
|
|
9
|
-
get: async (k: string) => store[k],
|
|
10
|
-
put: async (k: string, v: any) => { store[k] = v },
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("JotDB", () => {
|
|
16
|
-
let jot: JotDB;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
jot = new JotDB(createFakeState());
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("should set and get a value", async () => {
|
|
23
|
-
await jot.set("foo", "bar");
|
|
24
|
-
const val = await jot.get("foo");
|
|
25
|
-
assert(val === "bar");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should support setAll and getAll", async () => {
|
|
29
|
-
await jot.setAll({ a: 1, b: true });
|
|
30
|
-
const all = await jot.getAll();
|
|
31
|
-
assert(all.a === 1);
|
|
32
|
-
assert(all.b === true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should enforce schema after inference", async () => {
|
|
36
|
-
await jot.setAll({ x: "yes", y: 2 });
|
|
37
|
-
await assert.rejects(() => jot.set("y", "not a number"));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should strip unknowns if autoStrip is on", async () => {
|
|
41
|
-
await jot.setAll({ known: "yes" });
|
|
42
|
-
await jot.setOptions({ autoStrip: true });
|
|
43
|
-
await jot.setAll({ known: "ok", extra: "skip" });
|
|
44
|
-
const data = await jot.getAll();
|
|
45
|
-
assert(data.known === "ok");
|
|
46
|
-
assert(!("extra" in data));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("should block writes if readOnly is on", async () => {
|
|
50
|
-
await jot.setAll({ z: 9 });
|
|
51
|
-
await jot.setOptions({ readOnly: true });
|
|
52
|
-
await assert.rejects(() => jot.set("z", 10));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("should track audit logs", async () => {
|
|
56
|
-
await jot.set("a", 1);
|
|
57
|
-
await jot.setAll({ b: 2, c: 3 });
|
|
58
|
-
const log = await jot.getAuditLog();
|
|
59
|
-
assert(log.length >= 2);
|
|
60
|
-
assert(log[0].action === "setAll");
|
|
61
|
-
assert(log[1].action === "set");
|
|
62
|
-
});
|
|
63
|
-
});
|