jotdb 0.1.6 → 0.1.7
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/package.json +3 -2
- package/src/index.ts +186 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jotdb",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "wrangler dev --port 5173",
|
|
15
15
|
"deploy": "wrangler deploy",
|
|
16
|
-
"patch": "npm version patch
|
|
16
|
+
"patch": "npm version patch",
|
|
17
|
+
"publish": "npm publish --access public"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"typescript": "^5.8.3",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z, ZodTypeAny, ZodObject } from "zod";
|
|
1
|
+
import { z, ZodTypeAny, ZodObject, ZodError } from "zod";
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { cors } from 'hono/cors';
|
|
4
4
|
import { prettyJSON } from 'hono/pretty-json';
|
|
@@ -6,7 +6,9 @@ import { DurableObject } from "cloudflare:workers";
|
|
|
6
6
|
|
|
7
7
|
// Type definitions
|
|
8
8
|
type SchemaType = "string" | "number" | "boolean" | "email" | "array" | "object" | "any";
|
|
9
|
-
type
|
|
9
|
+
type ObjectSchema = Record<string, SchemaType>;
|
|
10
|
+
type ArraySchema = { __arrayType: SchemaType | ObjectSchema };
|
|
11
|
+
type SchemaDefinition = ObjectSchema | ArraySchema;
|
|
10
12
|
|
|
11
13
|
interface JotDBOptions {
|
|
12
14
|
autoStrip: boolean;
|
|
@@ -19,10 +21,39 @@ interface AuditLogEntry {
|
|
|
19
21
|
keys: string[];
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
function isZodError(e: any): e is ZodError {
|
|
25
|
+
return e && typeof e === 'object' && Array.isArray(e.issues);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleZod<T>(fn: () => T): T {
|
|
29
|
+
try {
|
|
30
|
+
return fn();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (isZodError(e)) {
|
|
33
|
+
throw new Error('Validation failed: ' + e.issues.map(issue => issue.message).join('; '));
|
|
34
|
+
}
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isArraySchema(schema: SchemaDefinition): schema is ArraySchema {
|
|
40
|
+
return '__arrayType' in schema;
|
|
41
|
+
}
|
|
42
|
+
function isObjectSchema(schema: SchemaDefinition): schema is ObjectSchema {
|
|
43
|
+
return !('__arrayType' in schema);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function inferPrimitiveType(v: any): SchemaType {
|
|
47
|
+
if (typeof v === "string") return v.includes("@") ? "email" : "string";
|
|
48
|
+
if (typeof v === "number") return "number";
|
|
49
|
+
if (typeof v === "boolean") return "boolean";
|
|
50
|
+
return "any";
|
|
51
|
+
}
|
|
52
|
+
|
|
22
53
|
export class JotDB extends DurableObject {
|
|
23
54
|
private data: Record<string, unknown> | unknown[] = {};
|
|
24
55
|
private rawSchema: SchemaDefinition = {};
|
|
25
|
-
private zodSchema:
|
|
56
|
+
private zodSchema: ZodTypeAny | null = null;
|
|
26
57
|
private options: JotDBOptions = {
|
|
27
58
|
autoStrip: false,
|
|
28
59
|
readOnly: false,
|
|
@@ -56,11 +87,49 @@ export class JotDB extends DurableObject {
|
|
|
56
87
|
return Array.isArray(this.data);
|
|
57
88
|
}
|
|
58
89
|
|
|
90
|
+
private inferSchemaFromValue(value: any): SchemaDefinition {
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
if (value.length === 0) return { __arrayType: "any" };
|
|
93
|
+
const first = value[0];
|
|
94
|
+
if (typeof first === "object" && first !== null && !Array.isArray(first)) {
|
|
95
|
+
// Array of objects
|
|
96
|
+
return { __arrayType: this.inferSchemaFromValue(first) };
|
|
97
|
+
}
|
|
98
|
+
// Array of primitives
|
|
99
|
+
return { __arrayType: inferPrimitiveType(first) };
|
|
100
|
+
}
|
|
101
|
+
if (typeof value === "object" && value !== null) {
|
|
102
|
+
const schema: ObjectSchema = {};
|
|
103
|
+
for (const [k, v] of Object.entries(value)) {
|
|
104
|
+
if (Array.isArray(v)) {
|
|
105
|
+
schema[k] = "array";
|
|
106
|
+
} else if (typeof v === "object" && v !== null) {
|
|
107
|
+
schema[k] = "object";
|
|
108
|
+
} else {
|
|
109
|
+
schema[k] = inferPrimitiveType(v);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return schema;
|
|
113
|
+
}
|
|
114
|
+
// Top-level primitive (shouldn't happen for objects, but fallback)
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
59
118
|
async push(item: unknown): Promise<void> {
|
|
60
119
|
await this.load();
|
|
61
120
|
if (!Array.isArray(this.data)) {
|
|
62
121
|
this.data = [];
|
|
63
122
|
}
|
|
123
|
+
if (!this.zodSchema) {
|
|
124
|
+
const schema = this.inferSchemaFromValue([item]);
|
|
125
|
+
await this.setSchema(schema);
|
|
126
|
+
console.info('[JotDB] Auto-inferred and set schema from first push:', schema);
|
|
127
|
+
}
|
|
128
|
+
if (isArraySchema(this.rawSchema)) {
|
|
129
|
+
handleZod(() => (this.zodSchema as any).parse([item]));
|
|
130
|
+
} else if (this.zodSchema && isObjectSchema(this.rawSchema)) {
|
|
131
|
+
handleZod(() => this.zodSchema!.parse(item));
|
|
132
|
+
}
|
|
64
133
|
(this.data as unknown[]).push(item);
|
|
65
134
|
await this.save();
|
|
66
135
|
await this.logAudit("push", []);
|
|
@@ -68,8 +137,17 @@ export class JotDB extends DurableObject {
|
|
|
68
137
|
|
|
69
138
|
async setAll(objOrArr: Record<string, unknown> | unknown[]): Promise<void> {
|
|
70
139
|
await this.load();
|
|
71
|
-
if (!
|
|
72
|
-
|
|
140
|
+
if (!this.zodSchema) {
|
|
141
|
+
const schema = this.inferSchemaFromValue(objOrArr);
|
|
142
|
+
await this.setSchema(schema);
|
|
143
|
+
console.info('[JotDB] Auto-inferred and set schema from first setAll:', schema);
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(objOrArr) && isArraySchema(this.rawSchema)) {
|
|
146
|
+
handleZod(() => (this.zodSchema as any).parse(objOrArr));
|
|
147
|
+
} else if (!Array.isArray(objOrArr) && this.zodSchema && isObjectSchema(this.rawSchema)) {
|
|
148
|
+
objOrArr = handleZod(() => this.zodSchema!.parse(objOrArr));
|
|
149
|
+
} else if (Array.isArray(objOrArr) && this.zodSchema && isObjectSchema(this.rawSchema)) {
|
|
150
|
+
objOrArr.forEach(item => handleZod(() => this.zodSchema!.parse(item)));
|
|
73
151
|
}
|
|
74
152
|
this.data = objOrArr;
|
|
75
153
|
await this.save();
|
|
@@ -183,30 +261,33 @@ export class JotDB extends DurableObject {
|
|
|
183
261
|
await this.ctx.storage.put("__audit__", []);
|
|
184
262
|
}
|
|
185
263
|
|
|
186
|
-
private buildZodSchema(schema: SchemaDefinition):
|
|
264
|
+
private buildZodSchema(schema: SchemaDefinition): ZodTypeAny {
|
|
265
|
+
if (isArraySchema(schema)) {
|
|
266
|
+
const t = schema.__arrayType;
|
|
267
|
+
if (typeof t === "string") {
|
|
268
|
+
switch (t) {
|
|
269
|
+
case "string": return z.string().array();
|
|
270
|
+
case "number": return z.number().array();
|
|
271
|
+
case "boolean": return z.boolean().array();
|
|
272
|
+
case "email": return z.string().email().array();
|
|
273
|
+
default: return z.any().array();
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// Array of objects
|
|
277
|
+
return this.buildZodSchema(t).array();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Object schema
|
|
187
281
|
const shape: Record<string, ZodTypeAny> = {};
|
|
188
282
|
for (const [key, type] of Object.entries(schema)) {
|
|
189
283
|
switch (type) {
|
|
190
|
-
case "string":
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
case "
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
shape[key] = z.boolean();
|
|
198
|
-
break;
|
|
199
|
-
case "email":
|
|
200
|
-
shape[key] = z.string().email();
|
|
201
|
-
break;
|
|
202
|
-
case "array":
|
|
203
|
-
shape[key] = z.array(z.any());
|
|
204
|
-
break;
|
|
205
|
-
case "object":
|
|
206
|
-
shape[key] = z.record(z.any());
|
|
207
|
-
break;
|
|
208
|
-
default:
|
|
209
|
-
shape[key] = z.any();
|
|
284
|
+
case "string": shape[key] = z.string(); break;
|
|
285
|
+
case "number": shape[key] = z.number(); break;
|
|
286
|
+
case "boolean": shape[key] = z.boolean(); break;
|
|
287
|
+
case "email": shape[key] = z.string().email(); break;
|
|
288
|
+
case "array": shape[key] = z.array(z.any()); break;
|
|
289
|
+
case "object": shape[key] = z.record(z.any()); break;
|
|
290
|
+
default: shape[key] = z.any();
|
|
210
291
|
}
|
|
211
292
|
}
|
|
212
293
|
return z.object(shape);
|
|
@@ -222,9 +303,15 @@ export class JotDB extends DurableObject {
|
|
|
222
303
|
throw new Error("Database is in read-only mode");
|
|
223
304
|
}
|
|
224
305
|
if (!Array.isArray(this.data)) {
|
|
306
|
+
// Auto-infer schema if not set
|
|
307
|
+
if (!this.zodSchema) {
|
|
308
|
+
const schema = this.inferSchemaFromValue({ [key]: value });
|
|
309
|
+
await this.setSchema(schema);
|
|
310
|
+
console.info('[JotDB] Auto-inferred and set schema from first set:', schema);
|
|
311
|
+
}
|
|
225
312
|
this.data[key] = value;
|
|
226
313
|
if (this.zodSchema) {
|
|
227
|
-
this.zodSchema
|
|
314
|
+
handleZod(() => this.zodSchema!.parse(this.data));
|
|
228
315
|
}
|
|
229
316
|
await this.save();
|
|
230
317
|
await this.logAudit("set", key);
|
|
@@ -246,7 +333,8 @@ app.use('*', prettyJSON());
|
|
|
246
333
|
|
|
247
334
|
// Test endpoint
|
|
248
335
|
app.get('/test', async (c) => {
|
|
249
|
-
const
|
|
336
|
+
const JOB_ID = Date.now().toString();
|
|
337
|
+
const id = c.env.JOTDB.idFromName(JOB_ID);
|
|
250
338
|
const db = c.env.JOTDB.get(id) as unknown as JotDB;
|
|
251
339
|
|
|
252
340
|
const results = {
|
|
@@ -256,6 +344,7 @@ app.get('/test', async (c) => {
|
|
|
256
344
|
};
|
|
257
345
|
|
|
258
346
|
try {
|
|
347
|
+
|
|
259
348
|
// Test 1: Basic set/get
|
|
260
349
|
await db.set("test1", "hello");
|
|
261
350
|
const value1 = await db.get("test1");
|
|
@@ -316,7 +405,7 @@ app.get('/test', async (c) => {
|
|
|
316
405
|
});
|
|
317
406
|
|
|
318
407
|
// Test 5: Array mode - setAll and getAll
|
|
319
|
-
const arrayId = c.env.JOTDB.idFromName("test-array");
|
|
408
|
+
const arrayId = c.env.JOTDB.idFromName(JOB_ID + "-test-array");
|
|
320
409
|
const arrayDb = c.env.JOTDB.get(arrayId) as unknown as JotDB;
|
|
321
410
|
await arrayDb.setAll([1, 2, 3]);
|
|
322
411
|
const arr = await arrayDb.getAll();
|
|
@@ -335,6 +424,67 @@ app.get('/test', async (c) => {
|
|
|
335
424
|
value: arr2
|
|
336
425
|
});
|
|
337
426
|
|
|
427
|
+
// --- Schema inference tests-- -
|
|
428
|
+
// Object mode
|
|
429
|
+
const objId = c.env.JOTDB.idFromName("schema-obj");
|
|
430
|
+
const objDb = c.env.JOTDB.get(objId) as unknown as JotDB;
|
|
431
|
+
await objDb.setAll({ foo: "bar", count: 1 });
|
|
432
|
+
const objSchema = await objDb.getSchema();
|
|
433
|
+
let objPassed = false;
|
|
434
|
+
if (!('__arrayType' in objSchema)) {
|
|
435
|
+
objPassed = (objSchema as any).foo === "string" && (objSchema as any).count === "number";
|
|
436
|
+
}
|
|
437
|
+
results.tests.push({
|
|
438
|
+
name: "Object mode: inferred schema",
|
|
439
|
+
passed: objPassed,
|
|
440
|
+
value: objSchema
|
|
441
|
+
});
|
|
442
|
+
let objError = null;
|
|
443
|
+
try {
|
|
444
|
+
await objDb.setAll({ foo: 123, count: "not a number" });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
objError = e instanceof Error ? e.message : String(e);
|
|
447
|
+
}
|
|
448
|
+
results.tests.push({
|
|
449
|
+
name: "Object mode: invalid shape fails",
|
|
450
|
+
passed: !!objError,
|
|
451
|
+
error: objError
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Array mode
|
|
455
|
+
const arrId = c.env.JOTDB.idFromName("schema-arr");
|
|
456
|
+
const arrDb = c.env.JOTDB.get(arrId) as unknown as JotDB;
|
|
457
|
+
await arrDb.setAll([{ foo: "bar", count: 1 }]);
|
|
458
|
+
const arrSchema = await arrDb.getSchema();
|
|
459
|
+
let arrPassed = false;
|
|
460
|
+
if ('__arrayType' in arrSchema && typeof arrSchema.__arrayType === 'object') {
|
|
461
|
+
arrPassed = arrSchema.__arrayType.foo === "string" && arrSchema.__arrayType.count === "number";
|
|
462
|
+
}
|
|
463
|
+
results.tests.push({
|
|
464
|
+
name: "Array mode: inferred schema",
|
|
465
|
+
passed: arrPassed,
|
|
466
|
+
value: arrSchema
|
|
467
|
+
});
|
|
468
|
+
let arrError = null;
|
|
469
|
+
try {
|
|
470
|
+
await arrDb.push({ foo: 123, count: "not a number" });
|
|
471
|
+
} catch (e) {
|
|
472
|
+
arrError = e instanceof Error ? e.message : String(e);
|
|
473
|
+
}
|
|
474
|
+
results.tests.push({
|
|
475
|
+
name: "Array mode: invalid item fails",
|
|
476
|
+
passed: !!arrError,
|
|
477
|
+
error: arrError
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Test 0: Clear database, set options to read-only: false
|
|
481
|
+
await db.clear();
|
|
482
|
+
results.tests.push({
|
|
483
|
+
name: "Clear database",
|
|
484
|
+
passed: true,
|
|
485
|
+
value: await db.getAll()
|
|
486
|
+
});
|
|
487
|
+
|
|
338
488
|
// Get audit log
|
|
339
489
|
results.auditLog = await db.getAuditLog();
|
|
340
490
|
|
|
@@ -364,9 +514,15 @@ app.get('/test', async (c) => {
|
|
|
364
514
|
|
|
365
515
|
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
|
|
366
516
|
} catch (error) {
|
|
517
|
+
console.error(error);
|
|
518
|
+
let errorMessage = error instanceof Error ? error.message : String(error);
|
|
519
|
+
if (isZodError(error)) {
|
|
520
|
+
console.error(error.issues);
|
|
521
|
+
errorMessage = error.issues.map(issue => issue.message).join('\n');
|
|
522
|
+
}
|
|
367
523
|
let html = `<!DOCTYPE html><html><head><title>JotDB Test Error</title></head><body>` +
|
|
368
524
|
`<h1 style="color:red">Error</h1>` +
|
|
369
|
-
`<pre>${
|
|
525
|
+
`<pre>${errorMessage}</pre>` +
|
|
370
526
|
`<h2>Partial Results</h2>` +
|
|
371
527
|
`<pre>${JSON.stringify(results.tests, null, 2)}</pre>` +
|
|
372
528
|
`<h2>Audit Log</h2>` +
|