digital-objects 1.0.0

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 (87) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +25 -0
  3. package/LICENSE +21 -0
  4. package/README.md +476 -0
  5. package/dist/ai-database-adapter.d.ts +49 -0
  6. package/dist/ai-database-adapter.d.ts.map +1 -0
  7. package/dist/ai-database-adapter.js +89 -0
  8. package/dist/ai-database-adapter.js.map +1 -0
  9. package/dist/errors.d.ts +47 -0
  10. package/dist/errors.d.ts.map +1 -0
  11. package/dist/errors.js +72 -0
  12. package/dist/errors.js.map +1 -0
  13. package/dist/http-schemas.d.ts +165 -0
  14. package/dist/http-schemas.d.ts.map +1 -0
  15. package/dist/http-schemas.js +55 -0
  16. package/dist/http-schemas.js.map +1 -0
  17. package/dist/index.d.ts +29 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +32 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/linguistic.d.ts +54 -0
  22. package/dist/linguistic.d.ts.map +1 -0
  23. package/dist/linguistic.js +226 -0
  24. package/dist/linguistic.js.map +1 -0
  25. package/dist/memory-provider.d.ts +46 -0
  26. package/dist/memory-provider.d.ts.map +1 -0
  27. package/dist/memory-provider.js +279 -0
  28. package/dist/memory-provider.js.map +1 -0
  29. package/dist/ns-client.d.ts +88 -0
  30. package/dist/ns-client.d.ts.map +1 -0
  31. package/dist/ns-client.js +253 -0
  32. package/dist/ns-client.js.map +1 -0
  33. package/dist/ns-exports.d.ts +23 -0
  34. package/dist/ns-exports.d.ts.map +1 -0
  35. package/dist/ns-exports.js +21 -0
  36. package/dist/ns-exports.js.map +1 -0
  37. package/dist/ns.d.ts +60 -0
  38. package/dist/ns.d.ts.map +1 -0
  39. package/dist/ns.js +818 -0
  40. package/dist/ns.js.map +1 -0
  41. package/dist/r2-persistence.d.ts +112 -0
  42. package/dist/r2-persistence.d.ts.map +1 -0
  43. package/dist/r2-persistence.js +252 -0
  44. package/dist/r2-persistence.js.map +1 -0
  45. package/dist/schema-validation.d.ts +80 -0
  46. package/dist/schema-validation.d.ts.map +1 -0
  47. package/dist/schema-validation.js +233 -0
  48. package/dist/schema-validation.js.map +1 -0
  49. package/dist/types.d.ts +184 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +26 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +55 -0
  54. package/src/ai-database-adapter.test.ts +610 -0
  55. package/src/ai-database-adapter.ts +189 -0
  56. package/src/benchmark.test.ts +109 -0
  57. package/src/errors.ts +91 -0
  58. package/src/http-schemas.ts +67 -0
  59. package/src/index.ts +87 -0
  60. package/src/linguistic.test.ts +1107 -0
  61. package/src/linguistic.ts +253 -0
  62. package/src/memory-provider.ts +470 -0
  63. package/src/ns-client.test.ts +1360 -0
  64. package/src/ns-client.ts +342 -0
  65. package/src/ns-exports.ts +23 -0
  66. package/src/ns.test.ts +1381 -0
  67. package/src/ns.ts +1215 -0
  68. package/src/provider.test.ts +675 -0
  69. package/src/r2-persistence.test.ts +263 -0
  70. package/src/r2-persistence.ts +367 -0
  71. package/src/schema-validation.test.ts +167 -0
  72. package/src/schema-validation.ts +330 -0
  73. package/src/types.ts +252 -0
  74. package/test/action-status.test.ts +42 -0
  75. package/test/batch-limits.test.ts +165 -0
  76. package/test/docs.test.ts +48 -0
  77. package/test/errors.test.ts +148 -0
  78. package/test/http-validation.test.ts +401 -0
  79. package/test/ns-client-errors.test.ts +208 -0
  80. package/test/ns-namespace.test.ts +307 -0
  81. package/test/performance.test.ts +168 -0
  82. package/test/schema-validation-error.test.ts +213 -0
  83. package/test/schema-validation.test.ts +440 -0
  84. package/test/search-escaping.test.ts +359 -0
  85. package/test/security.test.ts +322 -0
  86. package/tsconfig.json +10 -0
  87. package/wrangler.jsonc +16 -0
package/dist/ns.js ADDED
@@ -0,0 +1,818 @@
1
+ /**
2
+ * NS - Namespace Durable Object
3
+ *
4
+ * SQLite-based implementation of DigitalObjectsProvider for Cloudflare Workers.
5
+ * Each NS instance represents a namespace (tenant) with isolated data.
6
+ */
7
+ import { DEFAULT_LIMIT, MAX_LIMIT, validateDirection } from './types.js';
8
+ import { deriveNoun, deriveVerb } from './linguistic.js';
9
+ import { validateData } from './schema-validation.js';
10
+ import { NotFoundError, ValidationError, errorToResponse } from './errors.js';
11
+ import { ZodError } from 'zod';
12
+ import { NounDefinitionSchema, VerbDefinitionSchema, CreateThingSchema, UpdateThingSchema, PerformActionSchema, BatchCreateThingsSchema, BatchUpdateThingsSchema, BatchDeleteThingsSchema, BatchPerformActionsSchema, } from './http-schemas.js';
13
+ /**
14
+ * Convert a ZodError to a ValidationError
15
+ */
16
+ function zodErrorToValidationError(error) {
17
+ const fieldErrors = error.errors.map((issue) => ({
18
+ field: issue.path.join('.') || 'root',
19
+ message: issue.message,
20
+ }));
21
+ return new ValidationError('Request validation failed', fieldErrors);
22
+ }
23
+ /**
24
+ * Calculate effective limit with safety bounds
25
+ */
26
+ function effectiveLimit(requestedLimit) {
27
+ return Math.min(requestedLimit ?? DEFAULT_LIMIT, MAX_LIMIT);
28
+ }
29
+ // Whitelist of allowed orderBy fields for SQL injection prevention
30
+ const ALLOWED_ORDER_FIELDS = [
31
+ 'createdAt',
32
+ 'updatedAt',
33
+ 'id',
34
+ 'noun',
35
+ 'verb',
36
+ 'status',
37
+ 'name',
38
+ 'title',
39
+ ];
40
+ /**
41
+ * Validates an orderBy field name to prevent SQL injection.
42
+ * Allows whitelisted fields or simple alphanumeric field names.
43
+ */
44
+ function validateOrderByField(field) {
45
+ // Allow whitelisted fields
46
+ if (ALLOWED_ORDER_FIELDS.includes(field))
47
+ return true;
48
+ // Only allow simple alphanumeric field names (letters, numbers, underscores)
49
+ // Must start with a letter or underscore
50
+ return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field);
51
+ }
52
+ /**
53
+ * NS - Namespace Durable Object
54
+ */
55
+ export class NS {
56
+ sql;
57
+ initialized = false;
58
+ // Caches for noun and verb definitions to reduce database lookups
59
+ nounCache = new Map();
60
+ verbCache = new Map();
61
+ constructor(ctx, _env) {
62
+ this.sql = ctx.storage.sql;
63
+ }
64
+ async ensureInitialized() {
65
+ if (this.initialized)
66
+ return;
67
+ // Create tables
68
+ this.sql.exec(`
69
+ CREATE TABLE IF NOT EXISTS nouns (
70
+ name TEXT PRIMARY KEY,
71
+ singular TEXT NOT NULL,
72
+ plural TEXT NOT NULL,
73
+ slug TEXT NOT NULL,
74
+ description TEXT,
75
+ schema TEXT,
76
+ created_at INTEGER NOT NULL
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS verbs (
80
+ name TEXT PRIMARY KEY,
81
+ action TEXT NOT NULL,
82
+ act TEXT NOT NULL,
83
+ activity TEXT NOT NULL,
84
+ event TEXT NOT NULL,
85
+ reverse_by TEXT,
86
+ reverse_at TEXT,
87
+ reverse_in TEXT,
88
+ inverse TEXT,
89
+ description TEXT,
90
+ created_at INTEGER NOT NULL
91
+ );
92
+
93
+ CREATE TABLE IF NOT EXISTS things (
94
+ id TEXT PRIMARY KEY,
95
+ noun TEXT NOT NULL,
96
+ data TEXT NOT NULL,
97
+ created_at INTEGER NOT NULL,
98
+ updated_at INTEGER NOT NULL
99
+ );
100
+ CREATE INDEX IF NOT EXISTS idx_things_noun ON things(noun);
101
+
102
+ CREATE TABLE IF NOT EXISTS actions (
103
+ id TEXT PRIMARY KEY,
104
+ verb TEXT NOT NULL,
105
+ subject TEXT,
106
+ object TEXT,
107
+ data TEXT,
108
+ status TEXT NOT NULL DEFAULT 'completed',
109
+ created_at INTEGER NOT NULL,
110
+ completed_at INTEGER
111
+ );
112
+ CREATE INDEX IF NOT EXISTS idx_actions_verb ON actions(verb);
113
+ CREATE INDEX IF NOT EXISTS idx_actions_subject ON actions(subject);
114
+ CREATE INDEX IF NOT EXISTS idx_actions_object ON actions(object);
115
+ CREATE INDEX IF NOT EXISTS idx_actions_status ON actions(status);
116
+ `);
117
+ this.initialized = true;
118
+ }
119
+ // HTTP API handler
120
+ async fetch(request) {
121
+ await this.ensureInitialized();
122
+ const url = new URL(request.url);
123
+ const path = url.pathname;
124
+ const method = request.method;
125
+ try {
126
+ // Route to appropriate handler
127
+ if (path === '/nouns' && method === 'POST') {
128
+ const rawBody = await request.json();
129
+ const body = NounDefinitionSchema.parse(rawBody);
130
+ const noun = await this.defineNoun(body);
131
+ return Response.json(noun);
132
+ }
133
+ if (path.startsWith('/nouns/') && method === 'GET') {
134
+ const name = decodeURIComponent(path.slice('/nouns/'.length));
135
+ const noun = await this.getNoun(name);
136
+ return noun ? Response.json(noun) : new Response('Not found', { status: 404 });
137
+ }
138
+ if (path === '/nouns' && method === 'GET') {
139
+ const nouns = await this.listNouns();
140
+ return Response.json(nouns);
141
+ }
142
+ if (path === '/verbs' && method === 'POST') {
143
+ const rawBody = await request.json();
144
+ const body = VerbDefinitionSchema.parse(rawBody);
145
+ const verb = await this.defineVerb(body);
146
+ return Response.json(verb);
147
+ }
148
+ if (path.startsWith('/verbs/') && method === 'GET') {
149
+ const name = decodeURIComponent(path.slice('/verbs/'.length));
150
+ const verb = await this.getVerb(name);
151
+ return verb ? Response.json(verb) : new Response('Not found', { status: 404 });
152
+ }
153
+ if (path === '/verbs' && method === 'GET') {
154
+ const verbs = await this.listVerbs();
155
+ return Response.json(verbs);
156
+ }
157
+ if (path === '/things' && method === 'POST') {
158
+ const rawBody = await request.json();
159
+ const { noun, data, id } = CreateThingSchema.parse(rawBody);
160
+ const thing = await this.create(noun, data, id);
161
+ return Response.json(thing);
162
+ }
163
+ if (path.startsWith('/things/') && method === 'GET') {
164
+ const id = decodeURIComponent(path.slice('/things/'.length));
165
+ const thing = await this.get(id);
166
+ return thing ? Response.json(thing) : new Response('Not found', { status: 404 });
167
+ }
168
+ if (path.startsWith('/things/') && method === 'PATCH') {
169
+ const id = decodeURIComponent(path.slice('/things/'.length));
170
+ const rawBody = await request.json();
171
+ const { data } = UpdateThingSchema.parse(rawBody);
172
+ const thing = await this.update(id, data);
173
+ return Response.json(thing);
174
+ }
175
+ if (path.startsWith('/things/') && method === 'DELETE') {
176
+ const id = decodeURIComponent(path.slice('/things/'.length));
177
+ const deleted = await this.delete(id);
178
+ return Response.json({ deleted });
179
+ }
180
+ if (path === '/things' && method === 'GET') {
181
+ const noun = url.searchParams.get('noun');
182
+ if (!noun) {
183
+ return new Response('noun parameter required', { status: 400 });
184
+ }
185
+ const options = {};
186
+ const limit = url.searchParams.get('limit');
187
+ const offset = url.searchParams.get('offset');
188
+ const orderBy = url.searchParams.get('orderBy');
189
+ const order = url.searchParams.get('order');
190
+ if (limit)
191
+ options.limit = parseInt(limit, 10);
192
+ if (offset)
193
+ options.offset = parseInt(offset, 10);
194
+ if (orderBy)
195
+ options.orderBy = orderBy;
196
+ if (order === 'asc' || order === 'desc')
197
+ options.order = order;
198
+ const things = await this.list(noun, options);
199
+ return Response.json(things);
200
+ }
201
+ if (path === '/search' && method === 'GET') {
202
+ const query = url.searchParams.get('q') ?? '';
203
+ const options = {};
204
+ const limit = url.searchParams.get('limit');
205
+ if (limit)
206
+ options.limit = parseInt(limit, 10);
207
+ const things = await this.search(query, options);
208
+ return Response.json(things);
209
+ }
210
+ if (path === '/actions' && method === 'POST') {
211
+ const rawBody = await request.json();
212
+ const { verb, subject, object, data } = PerformActionSchema.parse(rawBody);
213
+ const action = await this.perform(verb, subject, object, data);
214
+ return Response.json(action);
215
+ }
216
+ if (path.startsWith('/actions/') && method === 'GET') {
217
+ const id = decodeURIComponent(path.slice('/actions/'.length));
218
+ const action = await this.getAction(id);
219
+ return action ? Response.json(action) : new Response('Not found', { status: 404 });
220
+ }
221
+ if (path.startsWith('/actions/') && method === 'DELETE') {
222
+ const id = decodeURIComponent(path.slice('/actions/'.length));
223
+ const deleted = await this.deleteAction(id);
224
+ return Response.json({ deleted });
225
+ }
226
+ if (path === '/actions' && method === 'GET') {
227
+ const options = {};
228
+ const verb = url.searchParams.get('verb');
229
+ const subject = url.searchParams.get('subject');
230
+ const object = url.searchParams.get('object');
231
+ const limit = url.searchParams.get('limit');
232
+ const status = url.searchParams.get('status');
233
+ if (verb)
234
+ options.verb = verb;
235
+ if (subject)
236
+ options.subject = subject;
237
+ if (object)
238
+ options.object = object;
239
+ if (limit)
240
+ options.limit = parseInt(limit, 10);
241
+ if (status)
242
+ options.status = status;
243
+ const actions = await this.listActions(options);
244
+ return Response.json(actions);
245
+ }
246
+ if (path.startsWith('/edges/') && method === 'GET') {
247
+ const id = decodeURIComponent(path.slice('/edges/'.length));
248
+ const verb = url.searchParams.get('verb') ?? undefined;
249
+ const directionParam = url.searchParams.get('direction') ?? 'out';
250
+ const direction = validateDirection(directionParam);
251
+ const edges = await this.edges(id, verb, direction);
252
+ return Response.json(edges);
253
+ }
254
+ if (path.startsWith('/related/') && method === 'GET') {
255
+ const id = decodeURIComponent(path.slice('/related/'.length));
256
+ const verb = url.searchParams.get('verb') ?? undefined;
257
+ const directionParam = url.searchParams.get('direction') ?? 'out';
258
+ const direction = validateDirection(directionParam);
259
+ const things = await this.related(id, verb, direction);
260
+ return Response.json(things);
261
+ }
262
+ // Batch operations
263
+ if (path === '/batch/things' && method === 'POST') {
264
+ const rawBody = await request.json();
265
+ const { noun, items } = BatchCreateThingsSchema.parse(rawBody);
266
+ const things = await this.createMany(noun, items);
267
+ return Response.json(things);
268
+ }
269
+ if (path === '/batch/things' && method === 'PATCH') {
270
+ const rawBody = await request.json();
271
+ const { updates } = BatchUpdateThingsSchema.parse(rawBody);
272
+ const things = await this.updateMany(updates);
273
+ return Response.json(things);
274
+ }
275
+ if (path === '/batch/things' && method === 'DELETE') {
276
+ const rawBody = await request.json();
277
+ const { ids } = BatchDeleteThingsSchema.parse(rawBody);
278
+ const results = await this.deleteMany(ids);
279
+ return Response.json(results);
280
+ }
281
+ if (path === '/batch/actions' && method === 'POST') {
282
+ const rawBody = await request.json();
283
+ const { actions } = BatchPerformActionsSchema.parse(rawBody);
284
+ const results = await this.performMany(actions);
285
+ return Response.json(results);
286
+ }
287
+ return Response.json({ error: 'NOT_FOUND', message: 'Endpoint not found' }, { status: 404 });
288
+ }
289
+ catch (error) {
290
+ // Convert ZodError to ValidationError for consistent error handling
291
+ const normalizedError = error instanceof ZodError ? zodErrorToValidationError(error) : error;
292
+ const { body, status } = errorToResponse(normalizedError);
293
+ return Response.json(body, { status });
294
+ }
295
+ }
296
+ // ==================== Nouns ====================
297
+ async defineNoun(def) {
298
+ await this.ensureInitialized();
299
+ const derived = deriveNoun(def.name);
300
+ const now = Date.now();
301
+ this.sql.exec(`INSERT OR REPLACE INTO nouns (name, singular, plural, slug, description, schema, created_at)
302
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, def.name, def.singular ?? derived.singular, def.plural ?? derived.plural, derived.slug, def.description ?? null, def.schema ? JSON.stringify(def.schema) : null, now);
303
+ const noun = {
304
+ name: def.name,
305
+ singular: def.singular ?? derived.singular,
306
+ plural: def.plural ?? derived.plural,
307
+ slug: derived.slug,
308
+ description: def.description,
309
+ schema: def.schema,
310
+ createdAt: new Date(now),
311
+ };
312
+ // Update cache
313
+ this.nounCache.set(def.name, noun);
314
+ return noun;
315
+ }
316
+ async getNoun(name) {
317
+ await this.ensureInitialized();
318
+ // Check cache first
319
+ const cached = this.nounCache.get(name);
320
+ if (cached)
321
+ return cached;
322
+ const rows = [...this.sql.exec('SELECT * FROM nouns WHERE name = ?', name)];
323
+ if (rows.length === 0)
324
+ return null;
325
+ const row = rows[0];
326
+ const noun = {
327
+ name: row.name,
328
+ singular: row.singular,
329
+ plural: row.plural,
330
+ slug: row.slug,
331
+ description: row.description,
332
+ schema: row.schema ? JSON.parse(row.schema) : undefined,
333
+ createdAt: new Date(row.created_at),
334
+ };
335
+ // Populate cache
336
+ this.nounCache.set(name, noun);
337
+ return noun;
338
+ }
339
+ async listNouns() {
340
+ await this.ensureInitialized();
341
+ const rows = [...this.sql.exec('SELECT * FROM nouns')];
342
+ return rows.map((row) => {
343
+ const r = row;
344
+ return {
345
+ name: r.name,
346
+ singular: r.singular,
347
+ plural: r.plural,
348
+ slug: r.slug,
349
+ description: r.description,
350
+ schema: r.schema ? JSON.parse(r.schema) : undefined,
351
+ createdAt: new Date(r.created_at),
352
+ };
353
+ });
354
+ }
355
+ // ==================== Verbs ====================
356
+ async defineVerb(def) {
357
+ await this.ensureInitialized();
358
+ const derived = deriveVerb(def.name);
359
+ const now = Date.now();
360
+ this.sql.exec(`INSERT OR REPLACE INTO verbs
361
+ (name, action, act, activity, event, reverse_by, reverse_at, reverse_in, inverse, description, created_at)
362
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, def.name, def.action ?? derived.action, def.act ?? derived.act, def.activity ?? derived.activity, def.event ?? derived.event, def.reverseBy ?? derived.reverseBy, derived.reverseAt, def.reverseIn ?? derived.reverseIn, def.inverse ?? null, def.description ?? null, now);
363
+ const verb = {
364
+ name: def.name,
365
+ action: def.action ?? derived.action,
366
+ act: def.act ?? derived.act,
367
+ activity: def.activity ?? derived.activity,
368
+ event: def.event ?? derived.event,
369
+ reverseBy: def.reverseBy ?? derived.reverseBy,
370
+ reverseAt: derived.reverseAt,
371
+ reverseIn: def.reverseIn ?? derived.reverseIn,
372
+ inverse: def.inverse,
373
+ description: def.description,
374
+ createdAt: new Date(now),
375
+ };
376
+ // Update cache
377
+ this.verbCache.set(def.name, verb);
378
+ return verb;
379
+ }
380
+ async getVerb(name) {
381
+ await this.ensureInitialized();
382
+ // Check cache first
383
+ const cached = this.verbCache.get(name);
384
+ if (cached)
385
+ return cached;
386
+ const rows = [...this.sql.exec('SELECT * FROM verbs WHERE name = ?', name)];
387
+ if (rows.length === 0)
388
+ return null;
389
+ const row = rows[0];
390
+ const verb = {
391
+ name: row.name,
392
+ action: row.action,
393
+ act: row.act,
394
+ activity: row.activity,
395
+ event: row.event,
396
+ reverseBy: row.reverse_by,
397
+ reverseAt: row.reverse_at,
398
+ reverseIn: row.reverse_in,
399
+ inverse: row.inverse,
400
+ description: row.description,
401
+ createdAt: new Date(row.created_at),
402
+ };
403
+ // Populate cache
404
+ this.verbCache.set(name, verb);
405
+ return verb;
406
+ }
407
+ async listVerbs() {
408
+ await this.ensureInitialized();
409
+ const rows = [...this.sql.exec('SELECT * FROM verbs')];
410
+ return rows.map((row) => {
411
+ const r = row;
412
+ return {
413
+ name: r.name,
414
+ action: r.action,
415
+ act: r.act,
416
+ activity: r.activity,
417
+ event: r.event,
418
+ reverseBy: r.reverse_by,
419
+ reverseAt: r.reverse_at,
420
+ reverseIn: r.reverse_in,
421
+ inverse: r.inverse,
422
+ description: r.description,
423
+ createdAt: new Date(r.created_at),
424
+ };
425
+ });
426
+ }
427
+ // ==================== Things ====================
428
+ async create(noun, data, id, options) {
429
+ await this.ensureInitialized();
430
+ // Validate data against noun schema if validation is enabled
431
+ if (options?.validate) {
432
+ const nounDef = await this.getNoun(noun);
433
+ validateData(data, nounDef?.schema, options);
434
+ }
435
+ const thingId = id ?? crypto.randomUUID();
436
+ const now = Date.now();
437
+ this.sql.exec(`INSERT INTO things (id, noun, data, created_at, updated_at)
438
+ VALUES (?, ?, ?, ?, ?)`, thingId, noun, JSON.stringify(data), now, now);
439
+ return {
440
+ id: thingId,
441
+ noun,
442
+ data,
443
+ createdAt: new Date(now),
444
+ updatedAt: new Date(now),
445
+ };
446
+ }
447
+ async get(id) {
448
+ await this.ensureInitialized();
449
+ const rows = [...this.sql.exec('SELECT * FROM things WHERE id = ?', id)];
450
+ if (rows.length === 0)
451
+ return null;
452
+ const row = rows[0];
453
+ return {
454
+ id: row.id,
455
+ noun: row.noun,
456
+ data: JSON.parse(row.data),
457
+ createdAt: new Date(row.created_at),
458
+ updatedAt: new Date(row.updated_at),
459
+ };
460
+ }
461
+ async list(noun, options) {
462
+ await this.ensureInitialized();
463
+ let sql = 'SELECT * FROM things WHERE noun = ?';
464
+ const params = [noun];
465
+ // Apply where filter in SQL using json_extract for better performance
466
+ if (options?.where) {
467
+ for (const [key, value] of Object.entries(options.where)) {
468
+ // Validate field name to prevent SQL injection
469
+ if (!validateOrderByField(key)) {
470
+ throw new Error(`Invalid where field: ${key}`);
471
+ }
472
+ sql += ` AND json_extract(data, '$.${key}') = ?`;
473
+ // json_extract returns strings unquoted, numbers as numbers, booleans as 0/1, null as NULL
474
+ params.push(value);
475
+ }
476
+ }
477
+ if (options?.orderBy) {
478
+ // Validate orderBy field to prevent SQL injection
479
+ if (!validateOrderByField(options.orderBy)) {
480
+ throw new Error(`Invalid orderBy field: ${options.orderBy}`);
481
+ }
482
+ sql += ` ORDER BY json_extract(data, '$.${options.orderBy}')`;
483
+ sql += options.order === 'desc' ? ' DESC' : ' ASC';
484
+ }
485
+ // Apply limit with safety bounds
486
+ const limit = effectiveLimit(options?.limit);
487
+ sql += ` LIMIT ?`;
488
+ params.push(limit);
489
+ if (options?.offset) {
490
+ sql += ` OFFSET ?`;
491
+ params.push(options.offset);
492
+ }
493
+ const rows = [...this.sql.exec(sql, ...params)];
494
+ const results = rows.map((row) => {
495
+ const r = row;
496
+ return {
497
+ id: r.id,
498
+ noun: r.noun,
499
+ data: JSON.parse(r.data),
500
+ createdAt: new Date(r.created_at),
501
+ updatedAt: new Date(r.updated_at),
502
+ };
503
+ });
504
+ return results;
505
+ }
506
+ async find(noun, where) {
507
+ return this.list(noun, { where: where });
508
+ }
509
+ async update(id, data, options) {
510
+ await this.ensureInitialized();
511
+ const existing = await this.get(id);
512
+ if (!existing)
513
+ throw new NotFoundError('Thing', id);
514
+ const updated = { ...existing.data, ...data };
515
+ // Validate merged data against noun schema if validation is enabled
516
+ if (options?.validate) {
517
+ const nounDef = await this.getNoun(existing.noun);
518
+ validateData(updated, nounDef?.schema, options);
519
+ }
520
+ const now = Date.now();
521
+ this.sql.exec(`UPDATE things SET data = ?, updated_at = ? WHERE id = ?`, JSON.stringify(updated), now, id);
522
+ return {
523
+ ...existing,
524
+ data: updated,
525
+ updatedAt: new Date(now),
526
+ };
527
+ }
528
+ async delete(id) {
529
+ await this.ensureInitialized();
530
+ const result = this.sql.exec('DELETE FROM things WHERE id = ?', id);
531
+ return result.rowsWritten > 0;
532
+ }
533
+ async search(query, options) {
534
+ await this.ensureInitialized();
535
+ const q = `%${query.toLowerCase()}%`;
536
+ let sql = `SELECT * FROM things WHERE LOWER(data) LIKE ?`;
537
+ const params = [q];
538
+ // Apply limit with safety bounds
539
+ const limit = effectiveLimit(options?.limit);
540
+ sql += ` LIMIT ?`;
541
+ params.push(limit);
542
+ const rows = [...this.sql.exec(sql, ...params)];
543
+ return rows.map((row) => {
544
+ const r = row;
545
+ return {
546
+ id: r.id,
547
+ noun: r.noun,
548
+ data: JSON.parse(r.data),
549
+ createdAt: new Date(r.created_at),
550
+ updatedAt: new Date(r.updated_at),
551
+ };
552
+ });
553
+ }
554
+ // ==================== Actions ====================
555
+ async perform(verb, subject, object, data) {
556
+ await this.ensureInitialized();
557
+ const id = crypto.randomUUID();
558
+ const now = Date.now();
559
+ this.sql.exec(`INSERT INTO actions (id, verb, subject, object, data, status, created_at, completed_at)
560
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, verb, subject ?? null, object ?? null, data ? JSON.stringify(data) : null, 'completed', now, now);
561
+ return {
562
+ id,
563
+ verb,
564
+ subject,
565
+ object,
566
+ data,
567
+ status: 'completed',
568
+ createdAt: new Date(now),
569
+ completedAt: new Date(now),
570
+ };
571
+ }
572
+ async getAction(id) {
573
+ await this.ensureInitialized();
574
+ const rows = [...this.sql.exec('SELECT * FROM actions WHERE id = ?', id)];
575
+ if (rows.length === 0)
576
+ return null;
577
+ const row = rows[0];
578
+ return {
579
+ id: row.id,
580
+ verb: row.verb,
581
+ subject: row.subject,
582
+ object: row.object,
583
+ data: row.data ? JSON.parse(row.data) : undefined,
584
+ status: row.status,
585
+ createdAt: new Date(row.created_at),
586
+ completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
587
+ };
588
+ }
589
+ async listActions(options) {
590
+ await this.ensureInitialized();
591
+ let sql = 'SELECT * FROM actions WHERE 1=1';
592
+ const params = [];
593
+ if (options?.verb) {
594
+ sql += ' AND verb = ?';
595
+ params.push(options.verb);
596
+ }
597
+ if (options?.subject) {
598
+ sql += ' AND subject = ?';
599
+ params.push(options.subject);
600
+ }
601
+ if (options?.object) {
602
+ sql += ' AND object = ?';
603
+ params.push(options.object);
604
+ }
605
+ if (options?.status) {
606
+ const statuses = Array.isArray(options.status) ? options.status : [options.status];
607
+ sql += ` AND status IN (${statuses.map(() => '?').join(', ')})`;
608
+ params.push(...statuses);
609
+ }
610
+ // Apply limit with safety bounds
611
+ const limit = effectiveLimit(options?.limit);
612
+ sql += ' LIMIT ?';
613
+ params.push(limit);
614
+ const rows = [...this.sql.exec(sql, ...params)];
615
+ return rows.map((row) => {
616
+ const r = row;
617
+ return {
618
+ id: r.id,
619
+ verb: r.verb,
620
+ subject: r.subject,
621
+ object: r.object,
622
+ data: r.data ? JSON.parse(r.data) : undefined,
623
+ status: r.status,
624
+ createdAt: new Date(r.created_at),
625
+ completedAt: r.completed_at ? new Date(r.completed_at) : undefined,
626
+ };
627
+ });
628
+ }
629
+ async deleteAction(id) {
630
+ await this.ensureInitialized();
631
+ const result = this.sql.exec('DELETE FROM actions WHERE id = ?', id);
632
+ return result.rowsWritten > 0;
633
+ }
634
+ // ==================== Graph Traversal ====================
635
+ async related(id, verb, direction = 'out', options) {
636
+ const validDirection = validateDirection(direction);
637
+ const edgesList = await this.edges(id, verb, validDirection);
638
+ const relatedIds = new Set();
639
+ for (const edge of edgesList) {
640
+ if (direction === 'out' || direction === 'both') {
641
+ if (edge.subject === id && edge.object) {
642
+ relatedIds.add(edge.object);
643
+ }
644
+ }
645
+ if (direction === 'in' || direction === 'both') {
646
+ if (edge.object === id && edge.subject) {
647
+ relatedIds.add(edge.subject);
648
+ }
649
+ }
650
+ }
651
+ let results = [];
652
+ for (const relatedId of relatedIds) {
653
+ const thing = await this.get(relatedId);
654
+ if (thing)
655
+ results.push(thing);
656
+ }
657
+ // Apply limit with safety bounds
658
+ const limit = effectiveLimit(options?.limit);
659
+ results = results.slice(0, limit);
660
+ return results;
661
+ }
662
+ async edges(id, verb, direction = 'out', options) {
663
+ const validDirection = validateDirection(direction);
664
+ await this.ensureInitialized();
665
+ let sql;
666
+ const params = [];
667
+ if (validDirection === 'out') {
668
+ sql = 'SELECT * FROM actions WHERE subject = ?';
669
+ params.push(id);
670
+ }
671
+ else if (validDirection === 'in') {
672
+ sql = 'SELECT * FROM actions WHERE object = ?';
673
+ params.push(id);
674
+ }
675
+ else {
676
+ sql = 'SELECT * FROM actions WHERE subject = ? OR object = ?';
677
+ params.push(id, id);
678
+ }
679
+ if (verb) {
680
+ sql += ' AND verb = ?';
681
+ params.push(verb);
682
+ }
683
+ // Apply limit with safety bounds
684
+ const limit = effectiveLimit(options?.limit);
685
+ sql += ' LIMIT ?';
686
+ params.push(limit);
687
+ const rows = [...this.sql.exec(sql, ...params)];
688
+ return rows.map((row) => {
689
+ const r = row;
690
+ return {
691
+ id: r.id,
692
+ verb: r.verb,
693
+ subject: r.subject,
694
+ object: r.object,
695
+ data: r.data ? JSON.parse(r.data) : undefined,
696
+ status: r.status,
697
+ createdAt: new Date(r.created_at),
698
+ completedAt: r.completed_at ? new Date(r.completed_at) : undefined,
699
+ };
700
+ });
701
+ }
702
+ // ==================== Batch Operations ====================
703
+ async createMany(noun, items) {
704
+ await this.ensureInitialized();
705
+ const now = Date.now();
706
+ const results = [];
707
+ // Use a transaction for atomic batch insert
708
+ this.sql.exec('BEGIN TRANSACTION');
709
+ try {
710
+ for (const item of items) {
711
+ const thingId = crypto.randomUUID();
712
+ this.sql.exec(`INSERT INTO things (id, noun, data, created_at, updated_at)
713
+ VALUES (?, ?, ?, ?, ?)`, thingId, noun, JSON.stringify(item), now, now);
714
+ results.push({
715
+ id: thingId,
716
+ noun,
717
+ data: item,
718
+ createdAt: new Date(now),
719
+ updatedAt: new Date(now),
720
+ });
721
+ }
722
+ this.sql.exec('COMMIT');
723
+ }
724
+ catch (error) {
725
+ this.sql.exec('ROLLBACK');
726
+ throw error;
727
+ }
728
+ return results;
729
+ }
730
+ async updateMany(updates) {
731
+ await this.ensureInitialized();
732
+ const now = Date.now();
733
+ const results = [];
734
+ // Use a transaction for atomic batch update
735
+ this.sql.exec('BEGIN TRANSACTION');
736
+ try {
737
+ for (const { id, data } of updates) {
738
+ const existing = await this.get(id);
739
+ if (!existing)
740
+ throw new NotFoundError('Thing', id);
741
+ const updated = { ...existing.data, ...data };
742
+ this.sql.exec(`UPDATE things SET data = ?, updated_at = ? WHERE id = ?`, JSON.stringify(updated), now, id);
743
+ results.push({
744
+ ...existing,
745
+ data: updated,
746
+ updatedAt: new Date(now),
747
+ });
748
+ }
749
+ this.sql.exec('COMMIT');
750
+ }
751
+ catch (error) {
752
+ this.sql.exec('ROLLBACK');
753
+ throw error;
754
+ }
755
+ return results;
756
+ }
757
+ async deleteMany(ids) {
758
+ await this.ensureInitialized();
759
+ const results = [];
760
+ // Use a transaction for atomic batch delete
761
+ this.sql.exec('BEGIN TRANSACTION');
762
+ try {
763
+ for (const id of ids) {
764
+ const result = this.sql.exec('DELETE FROM things WHERE id = ?', id);
765
+ results.push(result.rowsWritten > 0);
766
+ }
767
+ this.sql.exec('COMMIT');
768
+ }
769
+ catch (error) {
770
+ this.sql.exec('ROLLBACK');
771
+ throw error;
772
+ }
773
+ return results;
774
+ }
775
+ async performMany(actions) {
776
+ await this.ensureInitialized();
777
+ const now = Date.now();
778
+ const results = [];
779
+ // Use a transaction for atomic batch insert
780
+ this.sql.exec('BEGIN TRANSACTION');
781
+ try {
782
+ for (const action of actions) {
783
+ const id = crypto.randomUUID();
784
+ this.sql.exec(`INSERT INTO actions (id, verb, subject, object, data, status, created_at, completed_at)
785
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, action.verb, action.subject ?? null, action.object ?? null, action.data ? JSON.stringify(action.data) : null, 'completed', now, now);
786
+ results.push({
787
+ id,
788
+ verb: action.verb,
789
+ subject: action.subject,
790
+ object: action.object,
791
+ data: action.data,
792
+ status: 'completed',
793
+ createdAt: new Date(now),
794
+ completedAt: new Date(now),
795
+ });
796
+ }
797
+ this.sql.exec('COMMIT');
798
+ }
799
+ catch (error) {
800
+ this.sql.exec('ROLLBACK');
801
+ throw error;
802
+ }
803
+ return results;
804
+ }
805
+ async close() {
806
+ // No-op for Durable Objects (SQLite persists automatically)
807
+ }
808
+ }
809
+ export default {
810
+ async fetch(request, env) {
811
+ const url = new URL(request.url);
812
+ const namespaceId = url.searchParams.get('ns') ?? 'default';
813
+ const id = env.NS.idFromName(namespaceId);
814
+ const stub = env.NS.get(id);
815
+ return stub.fetch(request);
816
+ },
817
+ };
818
+ //# sourceMappingURL=ns.js.map