jotdb 0.1.8 → 0.1.10

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/src/index.ts DELETED
@@ -1,538 +0,0 @@
1
- import { z, ZodTypeAny, ZodObject, ZodError } from "zod";
2
- import { Hono } from 'hono';
3
- import { cors } from 'hono/cors';
4
- import { prettyJSON } from 'hono/pretty-json';
5
- import { DurableObject } from "cloudflare:workers";
6
-
7
- // Type definitions
8
- type SchemaType = "string" | "number" | "boolean" | "email" | "array" | "object" | "any";
9
- type ObjectSchema = Record<string, SchemaType>;
10
- type ArraySchema = { __arrayType: SchemaType | ObjectSchema };
11
- type SchemaDefinition = ObjectSchema | ArraySchema;
12
-
13
- interface JotDBOptions {
14
- autoStrip: boolean;
15
- readOnly: boolean;
16
- }
17
-
18
- interface AuditLogEntry {
19
- timestamp: number;
20
- action: string;
21
- keys: string[];
22
- }
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
-
53
- export class JotDB extends DurableObject {
54
- private data: Record<string, unknown> | unknown[] = {};
55
- private rawSchema: SchemaDefinition = {};
56
- private zodSchema: ZodTypeAny | null = null;
57
- private options: JotDBOptions = {
58
- autoStrip: false,
59
- readOnly: false,
60
- };
61
- private auditLog: AuditLogEntry[] = [];
62
-
63
- constructor(state: any, env: Env) {
64
- super(state, env);
65
- }
66
-
67
- async load(): Promise<void> {
68
- if (this.data == null || (typeof this.data === 'object' && Object.keys(this.data).length === 0)) {
69
- this.data = (await this.ctx.storage.get("data")) ?? {};
70
- }
71
- if (Object.keys(this.rawSchema).length === 0) {
72
- this.rawSchema = (await this.ctx.storage.get("__schema__")) || {};
73
- if (Object.keys(this.rawSchema).length > 0) {
74
- this.zodSchema = this.buildZodSchema(this.rawSchema);
75
- }
76
- }
77
- const storedOptions = await this.ctx.storage.get("__options__") as JotDBOptions | null;
78
- if (storedOptions) this.options = storedOptions;
79
- this.auditLog = (await this.ctx.storage.get("__audit__")) || [];
80
- }
81
-
82
- async save(): Promise<void> {
83
- await this.ctx.storage.put("data", this.data);
84
- }
85
-
86
- isArrayMode(): boolean {
87
- return Array.isArray(this.data);
88
- }
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
-
118
- async push(item: unknown): Promise<void> {
119
- await this.load();
120
- if (!Array.isArray(this.data)) {
121
- this.data = [];
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
- }
133
- (this.data as unknown[]).push(item);
134
- await this.save();
135
- await this.logAudit("push", []);
136
- }
137
-
138
- async setAll(objOrArr: Record<string, unknown> | unknown[]): Promise<void> {
139
- await this.load();
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)));
151
- }
152
- this.data = objOrArr;
153
- await this.save();
154
- await this.logAudit("setAll", Array.isArray(objOrArr) ? [] : Object.keys(objOrArr));
155
- }
156
-
157
- async getAll(): Promise<unknown> {
158
- await this.load();
159
- return this.data;
160
- }
161
-
162
- async logAudit(action: string, keys: string[] | string): Promise<void> {
163
- const entry: AuditLogEntry = {
164
- timestamp: Date.now(),
165
- action,
166
- keys: Array.isArray(keys) ? keys : [keys],
167
- };
168
- this.auditLog.unshift(entry);
169
- await this.ctx.storage.put("__audit__", this.auditLog.slice(0, 100)); // keep max 100 entries
170
- }
171
-
172
- async get<T = unknown>(key: string): Promise<T | undefined> {
173
- await this.load();
174
- if (!Array.isArray(this.data)) {
175
- return this.data[key] as T;
176
- }
177
- return undefined;
178
- }
179
-
180
- async delete(key: string): Promise<void> {
181
- await this.load();
182
- if (!Array.isArray(this.data)) {
183
- delete this.data[key];
184
- await this.save();
185
- await this.logAudit("delete", key);
186
- }
187
- }
188
-
189
- async clear(): Promise<void> {
190
- this.data = {};
191
- await this.save();
192
- await this.logAudit("clear", []);
193
- }
194
-
195
- async keys(): Promise<string[]> {
196
- await this.load();
197
- if (!Array.isArray(this.data)) {
198
- return Object.keys(this.data);
199
- }
200
- return [];
201
- }
202
-
203
- async has(key: string): Promise<boolean> {
204
- await this.load();
205
- if (!Array.isArray(this.data)) {
206
- return key in this.data;
207
- }
208
- return false;
209
- }
210
-
211
- async getSchema(): Promise<SchemaDefinition> {
212
- await this.load();
213
- return this.rawSchema;
214
- }
215
-
216
- private warnSchemaDiff(newSchema: SchemaDefinition): void {
217
- const current = this.rawSchema;
218
- for (const key in newSchema) {
219
- if (!(key in current)) console.warn(`[JotDB] New key added: ${key}`);
220
- else if (newSchema[key] !== current[key]) {
221
- console.warn(
222
- `[JotDB] Type changed for "${key}": ${current[key]} → ${newSchema[key]}`
223
- );
224
- }
225
- }
226
- for (const key in current) {
227
- if (!(key in newSchema)) {
228
- console.warn(`[JotDB] Key removed: ${key}`);
229
- }
230
- }
231
- }
232
-
233
- async setSchema(schemaObj: SchemaDefinition): Promise<void> {
234
- await this.load();
235
- if (Object.keys(this.rawSchema).length > 0) {
236
- this.warnSchemaDiff(schemaObj);
237
- }
238
- this.rawSchema = schemaObj;
239
- this.zodSchema = this.buildZodSchema(schemaObj);
240
- await this.ctx.storage.put("__schema__", schemaObj);
241
- }
242
-
243
- async setOptions(opts: Partial<JotDBOptions>): Promise<void> {
244
- await this.load();
245
- Object.assign(this.options, opts);
246
- await this.ctx.storage.put("__options__", this.options);
247
- }
248
-
249
- async getOptions(): Promise<JotDBOptions> {
250
- await this.load();
251
- return this.options;
252
- }
253
-
254
- async getAuditLog(): Promise<AuditLogEntry[]> {
255
- await this.load();
256
- return this.auditLog;
257
- }
258
-
259
- async clearAuditLog(): Promise<void> {
260
- this.auditLog = [];
261
- await this.ctx.storage.put("__audit__", []);
262
- }
263
-
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
281
- const shape: Record<string, ZodTypeAny> = {};
282
- for (const [key, type] of Object.entries(schema)) {
283
- switch (type) {
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();
291
- }
292
- }
293
- return z.object(shape);
294
- }
295
-
296
- async fetch(request: Request) {
297
- return new Response("Hello, World!");
298
- }
299
-
300
- async set(key: string, value: unknown): Promise<void> {
301
- await this.load();
302
- if (this.options.readOnly) {
303
- throw new Error("Database is in read-only mode");
304
- }
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
- }
312
- this.data[key] = value;
313
- if (this.zodSchema) {
314
- handleZod(() => this.zodSchema!.parse(this.data));
315
- }
316
- await this.save();
317
- await this.logAudit("set", key);
318
- } else {
319
- throw new Error("Cannot use set() in array mode");
320
- }
321
- }
322
- }
323
-
324
- export interface Env {
325
- JOTDB: DurableObjectNamespace;
326
- }
327
-
328
- const app = new Hono<{ Bindings: Env }>();
329
-
330
- // Middleware
331
- app.use('*', cors());
332
- app.use('*', prettyJSON());
333
-
334
- // Test endpoint
335
- app.get('/test', async (c) => {
336
- const JOB_ID = Date.now().toString();
337
- const id = c.env.JOTDB.idFromName(JOB_ID);
338
- const db = c.env.JOTDB.get(id) as unknown as JotDB;
339
-
340
- const results = {
341
- timestamp: Date.now(),
342
- tests: [] as any[],
343
- auditLog: [] as any[]
344
- };
345
-
346
- try {
347
-
348
- // Test 1: Basic set/get
349
- await db.set("test1", "hello");
350
- const value1 = await db.get("test1");
351
- results.tests.push({
352
- name: "Basic set/get",
353
- passed: value1 === "hello",
354
- value: value1
355
- });
356
-
357
- // Test 2: Schema validation
358
- await db.setSchema({
359
- name: "string",
360
- age: "number",
361
- email: "email"
362
- });
363
- await db.setAll({
364
- name: "John",
365
- age: 30,
366
- email: "john@example.com"
367
- });
368
- const all = await db.getAll() as { name: string, age: number, email: string };
369
- results.tests.push({
370
- name: "Schema validation",
371
- passed: all.name === "John" && all.age === 30,
372
- value: all
373
- });
374
-
375
- // Test 3: Read-only mode
376
- await db.setOptions({ readOnly: true });
377
- try {
378
- await db.set("test3", "should fail");
379
- results.tests.push({
380
- name: "Read-only mode",
381
- passed: false,
382
- error: "Should have thrown"
383
- });
384
- } catch (e) {
385
- results.tests.push({
386
- name: "Read-only mode",
387
- passed: true,
388
- error: e instanceof Error ? e.message : String(e)
389
- });
390
- }
391
-
392
- // Test 4: Auto-strip mode
393
- await db.setOptions({ readOnly: false, autoStrip: true });
394
- await db.setAll({
395
- name: "Jane",
396
- age: 25,
397
- email: "jane@example.com",
398
- extra: "should be stripped"
399
- });
400
- const stripped = await db.getAll() as { name: string, age: number, email: string };
401
- results.tests.push({
402
- name: "Auto-strip mode",
403
- passed: !("extra" in stripped),
404
- value: stripped
405
- });
406
-
407
- // Test 5: Array mode - setAll and getAll
408
- const arrayId = c.env.JOTDB.idFromName(JOB_ID + "-test-array");
409
- const arrayDb = c.env.JOTDB.get(arrayId) as unknown as JotDB;
410
- await arrayDb.setAll([1, 2, 3]);
411
- const arr = await arrayDb.getAll();
412
- results.tests.push({
413
- name: "Array mode setAll/getAll",
414
- passed: Array.isArray(arr) && arr.length === 3 && arr[0] === 1 && arr[2] === 3,
415
- value: arr
416
- });
417
-
418
- // Test 6: Array mode - push
419
- await arrayDb.push(4);
420
- const arr2 = await arrayDb.getAll();
421
- results.tests.push({
422
- name: "Array mode push",
423
- passed: Array.isArray(arr2) && arr2.length === 4 && arr2[3] === 4,
424
- value: arr2
425
- });
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
-
488
- // Get audit log
489
- results.auditLog = await db.getAuditLog();
490
-
491
- // HTML output
492
- let html = `<!DOCTYPE html><html><head><title>JotDB Test Results</title>
493
- <style>
494
- body { font-family: sans-serif; margin: 2em; }
495
- .pass { color: green; }
496
- .fail { color: red; }
497
- .test { margin-bottom: 1em; }
498
- pre { background: #f4f4f4; padding: 0.5em; }
499
- </style>
500
- </head><body>
501
- <h1>JotDB Test Results</h1>
502
- <p><b>Timestamp:</b> ${new Date(results.timestamp).toLocaleString()}</p>
503
- <div>
504
- ${results.tests.map(test => `
505
- <div class="test">
506
- <b>${test.name}:</b> <span class="${test.passed ? 'pass' : 'fail'}">${test.passed ? 'PASS' : 'FAIL'}</span><br/>
507
- <pre>${JSON.stringify(test.value ?? test.error, null, 2)}</pre>
508
- </div>
509
- `).join('')}
510
- </div>
511
- <h2>Audit Log</h2>
512
- <pre>${JSON.stringify(results.auditLog, null, 2)}</pre>
513
- </body></html>`;
514
-
515
- return new Response(html, { headers: { 'Content-Type': 'text/html' } });
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
- }
523
- let html = `<!DOCTYPE html><html><head><title>JotDB Test Error</title></head><body>` +
524
- `<h1 style="color:red">Error</h1>` +
525
- `<pre>${errorMessage}</pre>` +
526
- `<h2>Partial Results</h2>` +
527
- `<pre>${JSON.stringify(results.tests, null, 2)}</pre>` +
528
- `<h2>Audit Log</h2>` +
529
- `<pre>${JSON.stringify(results.auditLog, null, 2)}</pre>` +
530
- `</body></html>`;
531
- return new Response(html, { headers: { 'Content-Type': 'text/html' } });
532
- }
533
- });
534
-
535
- // Health check endpoint
536
- app.get('/', (c) => c.text('JotDB Durable Object'));
537
-
538
- export default app;
package/tsconfig.json DELETED
@@ -1,33 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "ESNext",
5
- "lib": [
6
- "ES2020"
7
- ],
8
- "types": [
9
- "@cloudflare/workers-types"
10
- ],
11
- "declaration": true,
12
- "emitDeclarationOnly": false,
13
- "outDir": "dist",
14
- "moduleResolution": "node",
15
- "strict": true,
16
- "noImplicitAny": true,
17
- "strictNullChecks": true,
18
- "strictFunctionTypes": true,
19
- "strictBindCallApply": true,
20
- "strictPropertyInitialization": true,
21
- "noImplicitThis": true,
22
- "alwaysStrict": true,
23
- "esModuleInterop": true,
24
- "skipLibCheck": true,
25
- "forceConsistentCasingInFileNames": true
26
- },
27
- "include": [
28
- "src/**/*"
29
- ],
30
- "exclude": [
31
- "node_modules"
32
- ]
33
- }
package/wrangler.jsonc DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "$schema": "node_modules/wrangler/config-schema.json",
3
- "name": "jotdb",
4
- "main": "src/index.ts",
5
- "compatibility_date": "2024-05-01",
6
- "compatibility_flags": [
7
- "nodejs_compat"
8
- ],
9
- "durable_objects": {
10
- "bindings": [
11
- {
12
- "name": "JOTDB",
13
- "class_name": "JotDB"
14
- }
15
- ]
16
- },
17
- "migrations": [
18
- {
19
- "tag": "v1",
20
- "new_classes": [
21
- "JotDB"
22
- ]
23
- }
24
- ],
25
- "minify": true,
26
- "observability": {
27
- "enabled": true
28
- }
29
- }