jotdb 0.1.3 → 0.1.4

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/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: DurableObjectState, env: Env);
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
- return this.data[key];
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
- if (this.zodSchema) {
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
- delete this.data[key];
102
- await this.save();
103
- await this.logAudit("delete", key);
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
- return Object.keys(this.data);
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
- return key in this.data;
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",
3
+ "version": "0.1.4",
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 --remote",
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
- async logAudit(action: string, keys: string[] | string): Promise<void> {
57
- const entry: AuditLogEntry = {
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 getAll(): Promise<Record<string, unknown>> {
59
+ async push(item: unknown): Promise<void> {
72
60
  await this.load();
73
- return this.data;
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 set<T>(key: string, value: T): Promise<void> {
69
+ async setAll(objOrArr: Record<string, unknown> | unknown[]): Promise<void> {
77
70
  await this.load();
78
- if (this.options.readOnly) throw new Error("JotDB is in read-only mode");
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[key] = value;
74
+ this.data = objOrArr;
85
75
  await this.save();
86
- await this.logAudit("set", key);
76
+ await this.logAudit("setAll", Array.isArray(objOrArr) ? [] : Object.keys(objOrArr));
87
77
  }
88
78
 
89
- async setAll(obj: Record<string, unknown>): Promise<void> {
79
+ async getAll(): Promise<unknown> {
90
80
  await this.load();
91
- if (this.options.readOnly) throw new Error("JotDB is in read-only mode");
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
- if (!this.zodSchema) {
105
- const inferred: SchemaDefinition = {};
106
- for (const [k, v] of Object.entries(obj)) {
107
- inferred[k] = typeOfValue(v) as SchemaType;
108
- }
109
- await this.setSchema(inferred);
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
- if (this.zodSchema) {
113
- if (this.options.autoStrip) {
114
- obj = this.zodSchema.parse(obj); // returns stripped
115
- } else {
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
- delete this.data[key];
128
- await this.save();
129
- await this.logAudit("delete", key);
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
- return Object.keys(this.data);
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
- return key in this.data;
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
- return c.json(results);
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
- return c.json({
322
- error: error instanceof Error ? error.message : String(error),
323
- tests: results.tests,
324
- auditLog: results.auditLog
325
- }, 500);
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
- });