jotdb 0.1.0 → 0.1.2
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 +5 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +279 -0
- package/jotdb.tests.ts +1 -1
- package/package.json +15 -3
- package/src/index.ts +5 -4
- package/tsconfig.json +4 -1
- package/wrangler.jsonc +0 -1
package/bun.lock
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
"workspaces": {
|
|
4
4
|
"": {
|
|
5
5
|
"name": "jotdb",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"typescript": "^5.8.3",
|
|
8
|
+
},
|
|
6
9
|
"devDependencies": {
|
|
7
10
|
"@cloudflare/workers-types": "^4.20250517.0",
|
|
8
11
|
"hono": "^4.7.10",
|
|
@@ -196,6 +199,8 @@
|
|
|
196
199
|
|
|
197
200
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
198
201
|
|
|
202
|
+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
|
203
|
+
|
|
199
204
|
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
|
200
205
|
|
|
201
206
|
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DurableObjectState } from "@cloudflare/workers-types";
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { DurableObject } from "cloudflare:workers";
|
|
4
|
+
type SchemaType = "string" | "number" | "boolean" | "email" | "array" | "object" | "any";
|
|
5
|
+
type SchemaDefinition = Record<string, SchemaType>;
|
|
6
|
+
interface JotDBOptions {
|
|
7
|
+
autoStrip: boolean;
|
|
8
|
+
readOnly: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface AuditLogEntry {
|
|
11
|
+
timestamp: number;
|
|
12
|
+
action: string;
|
|
13
|
+
keys: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare class JotDB extends DurableObject {
|
|
16
|
+
private data;
|
|
17
|
+
private rawSchema;
|
|
18
|
+
private zodSchema;
|
|
19
|
+
private options;
|
|
20
|
+
private auditLog;
|
|
21
|
+
constructor(state: DurableObjectState, env: Env);
|
|
22
|
+
load(): Promise<void>;
|
|
23
|
+
save(): Promise<void>;
|
|
24
|
+
logAudit(action: string, keys: string[] | string): Promise<void>;
|
|
25
|
+
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
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
clear(): Promise<void>;
|
|
31
|
+
keys(): Promise<string[]>;
|
|
32
|
+
has(key: string): Promise<boolean>;
|
|
33
|
+
getSchema(): Promise<SchemaDefinition>;
|
|
34
|
+
private warnSchemaDiff;
|
|
35
|
+
setSchema(schemaObj: SchemaDefinition): Promise<void>;
|
|
36
|
+
setOptions(opts: Partial<JotDBOptions>): Promise<void>;
|
|
37
|
+
getOptions(): Promise<JotDBOptions>;
|
|
38
|
+
getAuditLog(): Promise<AuditLogEntry[]>;
|
|
39
|
+
clearAuditLog(): Promise<void>;
|
|
40
|
+
private buildZodSchema;
|
|
41
|
+
fetch(request: Request): Promise<Response>;
|
|
42
|
+
}
|
|
43
|
+
export interface Env {
|
|
44
|
+
JOTDB: DurableObjectNamespace;
|
|
45
|
+
}
|
|
46
|
+
declare const app: Hono<{
|
|
47
|
+
Bindings: Env;
|
|
48
|
+
}, import("hono/types").BlankSchema, "/">;
|
|
49
|
+
export default app;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { z } 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
|
+
export class JotDB extends DurableObject {
|
|
7
|
+
constructor(state, env) {
|
|
8
|
+
super(state, env);
|
|
9
|
+
this.data = {};
|
|
10
|
+
this.rawSchema = {};
|
|
11
|
+
this.zodSchema = null;
|
|
12
|
+
this.options = {
|
|
13
|
+
autoStrip: false,
|
|
14
|
+
readOnly: false,
|
|
15
|
+
};
|
|
16
|
+
this.auditLog = [];
|
|
17
|
+
}
|
|
18
|
+
async load() {
|
|
19
|
+
if (Object.keys(this.data).length === 0) {
|
|
20
|
+
this.data = (await this.ctx.storage.get("data")) || {};
|
|
21
|
+
}
|
|
22
|
+
if (Object.keys(this.rawSchema).length === 0) {
|
|
23
|
+
this.rawSchema = (await this.ctx.storage.get("__schema__")) || {};
|
|
24
|
+
if (Object.keys(this.rawSchema).length > 0) {
|
|
25
|
+
this.zodSchema = this.buildZodSchema(this.rawSchema);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const storedOptions = await this.ctx.storage.get("__options__");
|
|
29
|
+
if (storedOptions)
|
|
30
|
+
this.options = storedOptions;
|
|
31
|
+
this.auditLog = (await this.ctx.storage.get("__audit__")) || [];
|
|
32
|
+
}
|
|
33
|
+
async save() {
|
|
34
|
+
await this.ctx.storage.put("data", this.data);
|
|
35
|
+
}
|
|
36
|
+
async logAudit(action, keys) {
|
|
37
|
+
const entry = {
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
action,
|
|
40
|
+
keys: Array.isArray(keys) ? keys : [keys],
|
|
41
|
+
};
|
|
42
|
+
this.auditLog.unshift(entry);
|
|
43
|
+
await this.ctx.storage.put("__audit__", this.auditLog.slice(0, 100)); // keep max 100 entries
|
|
44
|
+
}
|
|
45
|
+
async get(key) {
|
|
46
|
+
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);
|
|
86
|
+
}
|
|
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));
|
|
98
|
+
}
|
|
99
|
+
async delete(key) {
|
|
100
|
+
await this.load();
|
|
101
|
+
delete this.data[key];
|
|
102
|
+
await this.save();
|
|
103
|
+
await this.logAudit("delete", key);
|
|
104
|
+
}
|
|
105
|
+
async clear() {
|
|
106
|
+
this.data = {};
|
|
107
|
+
await this.save();
|
|
108
|
+
await this.logAudit("clear", []);
|
|
109
|
+
}
|
|
110
|
+
async keys() {
|
|
111
|
+
await this.load();
|
|
112
|
+
return Object.keys(this.data);
|
|
113
|
+
}
|
|
114
|
+
async has(key) {
|
|
115
|
+
await this.load();
|
|
116
|
+
return key in this.data;
|
|
117
|
+
}
|
|
118
|
+
async getSchema() {
|
|
119
|
+
await this.load();
|
|
120
|
+
return this.rawSchema;
|
|
121
|
+
}
|
|
122
|
+
warnSchemaDiff(newSchema) {
|
|
123
|
+
const current = this.rawSchema;
|
|
124
|
+
for (const key in newSchema) {
|
|
125
|
+
if (!(key in current))
|
|
126
|
+
console.warn(`[JotDB] New key added: ${key}`);
|
|
127
|
+
else if (newSchema[key] !== current[key]) {
|
|
128
|
+
console.warn(`[JotDB] Type changed for "${key}": ${current[key]} → ${newSchema[key]}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const key in current) {
|
|
132
|
+
if (!(key in newSchema)) {
|
|
133
|
+
console.warn(`[JotDB] Key removed: ${key}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async setSchema(schemaObj) {
|
|
138
|
+
await this.load();
|
|
139
|
+
if (Object.keys(this.rawSchema).length > 0) {
|
|
140
|
+
this.warnSchemaDiff(schemaObj);
|
|
141
|
+
}
|
|
142
|
+
this.rawSchema = schemaObj;
|
|
143
|
+
this.zodSchema = this.buildZodSchema(schemaObj);
|
|
144
|
+
await this.ctx.storage.put("__schema__", schemaObj);
|
|
145
|
+
}
|
|
146
|
+
async setOptions(opts) {
|
|
147
|
+
await this.load();
|
|
148
|
+
Object.assign(this.options, opts);
|
|
149
|
+
await this.ctx.storage.put("__options__", this.options);
|
|
150
|
+
}
|
|
151
|
+
async getOptions() {
|
|
152
|
+
await this.load();
|
|
153
|
+
return this.options;
|
|
154
|
+
}
|
|
155
|
+
async getAuditLog() {
|
|
156
|
+
await this.load();
|
|
157
|
+
return this.auditLog;
|
|
158
|
+
}
|
|
159
|
+
async clearAuditLog() {
|
|
160
|
+
this.auditLog = [];
|
|
161
|
+
await this.ctx.storage.put("__audit__", []);
|
|
162
|
+
}
|
|
163
|
+
buildZodSchema(schema) {
|
|
164
|
+
const shape = {};
|
|
165
|
+
for (const [key, type] of Object.entries(schema)) {
|
|
166
|
+
switch (type) {
|
|
167
|
+
case "string":
|
|
168
|
+
shape[key] = z.string();
|
|
169
|
+
break;
|
|
170
|
+
case "number":
|
|
171
|
+
shape[key] = z.number();
|
|
172
|
+
break;
|
|
173
|
+
case "boolean":
|
|
174
|
+
shape[key] = z.boolean();
|
|
175
|
+
break;
|
|
176
|
+
case "email":
|
|
177
|
+
shape[key] = z.string().email();
|
|
178
|
+
break;
|
|
179
|
+
case "array":
|
|
180
|
+
shape[key] = z.array(z.any());
|
|
181
|
+
break;
|
|
182
|
+
case "object":
|
|
183
|
+
shape[key] = z.record(z.any());
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
shape[key] = z.any();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return z.object(shape);
|
|
190
|
+
}
|
|
191
|
+
async fetch(request) {
|
|
192
|
+
return new Response("Hello, World!");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const app = new Hono();
|
|
196
|
+
// Middleware
|
|
197
|
+
app.use('*', cors());
|
|
198
|
+
app.use('*', prettyJSON());
|
|
199
|
+
// Test endpoint
|
|
200
|
+
app.get('/test', async (c) => {
|
|
201
|
+
const id = c.env.JOTDB.idFromName("test-db");
|
|
202
|
+
const db = c.env.JOTDB.get(id);
|
|
203
|
+
const results = {
|
|
204
|
+
timestamp: Date.now(),
|
|
205
|
+
tests: [],
|
|
206
|
+
auditLog: []
|
|
207
|
+
};
|
|
208
|
+
try {
|
|
209
|
+
// Test 1: Basic set/get
|
|
210
|
+
await db.set("test1", "hello");
|
|
211
|
+
const value1 = await db.get("test1");
|
|
212
|
+
results.tests.push({
|
|
213
|
+
name: "Basic set/get",
|
|
214
|
+
passed: value1 === "hello",
|
|
215
|
+
value: value1
|
|
216
|
+
});
|
|
217
|
+
// Test 2: Schema validation
|
|
218
|
+
await db.setSchema({
|
|
219
|
+
name: "string",
|
|
220
|
+
age: "number",
|
|
221
|
+
email: "email"
|
|
222
|
+
});
|
|
223
|
+
await db.setAll({
|
|
224
|
+
name: "John",
|
|
225
|
+
age: 30,
|
|
226
|
+
email: "john@example.com"
|
|
227
|
+
});
|
|
228
|
+
const all = await db.getAll();
|
|
229
|
+
results.tests.push({
|
|
230
|
+
name: "Schema validation",
|
|
231
|
+
passed: all.name === "John" && all.age === 30,
|
|
232
|
+
value: all
|
|
233
|
+
});
|
|
234
|
+
// Test 3: Read-only mode
|
|
235
|
+
await db.setOptions({ readOnly: true });
|
|
236
|
+
try {
|
|
237
|
+
await db.set("test3", "should fail");
|
|
238
|
+
results.tests.push({
|
|
239
|
+
name: "Read-only mode",
|
|
240
|
+
passed: false,
|
|
241
|
+
error: "Should have thrown"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
results.tests.push({
|
|
246
|
+
name: "Read-only mode",
|
|
247
|
+
passed: true,
|
|
248
|
+
error: e instanceof Error ? e.message : String(e)
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
// Test 4: Auto-strip mode
|
|
252
|
+
await db.setOptions({ readOnly: false, autoStrip: true });
|
|
253
|
+
await db.setAll({
|
|
254
|
+
name: "Jane",
|
|
255
|
+
age: 25,
|
|
256
|
+
email: "jane@example.com",
|
|
257
|
+
extra: "should be stripped"
|
|
258
|
+
});
|
|
259
|
+
const stripped = await db.getAll();
|
|
260
|
+
results.tests.push({
|
|
261
|
+
name: "Auto-strip mode",
|
|
262
|
+
passed: !("extra" in stripped),
|
|
263
|
+
value: stripped
|
|
264
|
+
});
|
|
265
|
+
// Get audit log
|
|
266
|
+
results.auditLog = await db.getAuditLog();
|
|
267
|
+
return c.json(results);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return c.json({
|
|
271
|
+
error: error instanceof Error ? error.message : String(error),
|
|
272
|
+
tests: results.tests,
|
|
273
|
+
auditLog: results.auditLog
|
|
274
|
+
}, 500);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
// Health check endpoint
|
|
278
|
+
app.get('/', (c) => c.text('JotDB Durable Object'));
|
|
279
|
+
export default app;
|
package/jotdb.tests.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jotdb",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
5
10
|
"scripts": {
|
|
6
11
|
"dev": "wrangler dev --port 5173 --remote",
|
|
7
12
|
"test": "bun test jotdb.tests.ts",
|
|
8
|
-
"deploy": "wrangler deploy"
|
|
13
|
+
"deploy": "wrangler deploy",
|
|
14
|
+
"patch": "npm version patch && npm publish --access public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"typescript": "^5.8.3"
|
|
9
18
|
},
|
|
10
|
-
"dependencies": {},
|
|
11
19
|
"devDependencies": {
|
|
12
20
|
"@cloudflare/workers-types": "^4.20250517.0",
|
|
13
21
|
"hono": "^4.7.10",
|
|
14
22
|
"wrangler": "^4.15.2",
|
|
15
23
|
"zod": "^3.24.4"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/acoyfellow/jotdb.git"
|
|
16
28
|
}
|
|
17
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z, ZodTypeAny, ZodObject } from "zod";
|
|
2
|
-
import type { DurableObjectState } from "@cloudflare/workers-types";
|
|
3
2
|
import { Hono } from 'hono';
|
|
4
3
|
import { cors } from 'hono/cors';
|
|
5
4
|
import { prettyJSON } from 'hono/pretty-json';
|
|
@@ -21,7 +20,6 @@ interface AuditLogEntry {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export class JotDB extends DurableObject {
|
|
24
|
-
private ctx: DurableObjectState;
|
|
25
23
|
private data: Record<string, unknown> = {};
|
|
26
24
|
private rawSchema: SchemaDefinition = {};
|
|
27
25
|
private zodSchema: ZodObject<any> | null = null;
|
|
@@ -31,9 +29,8 @@ export class JotDB extends DurableObject {
|
|
|
31
29
|
};
|
|
32
30
|
private auditLog: AuditLogEntry[] = [];
|
|
33
31
|
|
|
34
|
-
constructor(state:
|
|
32
|
+
constructor(state: any, env: Env) {
|
|
35
33
|
super(state, env);
|
|
36
|
-
this.ctx = state;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
async load(): Promise<void> {
|
|
@@ -229,6 +226,10 @@ export class JotDB extends DurableObject {
|
|
|
229
226
|
}
|
|
230
227
|
return z.object(shape);
|
|
231
228
|
}
|
|
229
|
+
|
|
230
|
+
async fetch(request: Request) {
|
|
231
|
+
return new Response("Hello, World!");
|
|
232
|
+
}
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
export interface Env {
|
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
|
-
"module": "
|
|
4
|
+
"module": "ESNext",
|
|
5
5
|
"lib": [
|
|
6
6
|
"ES2020"
|
|
7
7
|
],
|
|
8
8
|
"types": [
|
|
9
9
|
"@cloudflare/workers-types"
|
|
10
10
|
],
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"emitDeclarationOnly": false,
|
|
13
|
+
"outDir": "dist",
|
|
11
14
|
"moduleResolution": "node",
|
|
12
15
|
"strict": true,
|
|
13
16
|
"noImplicitAny": true,
|