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.
Files changed (2) hide show
  1. package/package.json +3 -2
  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.6",
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 && npm publish --access public"
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 SchemaDefinition = Record<string, SchemaType>;
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: ZodObject<any> | null = null;
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 (!Array.isArray(objOrArr) && this.zodSchema) {
72
- objOrArr = this.zodSchema.parse(objOrArr);
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): ZodObject<any> {
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
- shape[key] = z.string();
192
- break;
193
- case "number":
194
- shape[key] = z.number();
195
- break;
196
- case "boolean":
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.parse(this.data);
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 id = c.env.JOTDB.idFromName("test-db");
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>${error instanceof Error ? error.message : String(error)}</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>` +