ai-database 2.1.3 → 2.3.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 (260) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +880 -669
  3. package/dist/actions.d.ts +2 -2
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +1 -1
  6. package/dist/actions.js.map +1 -1
  7. package/dist/ai-promise-db.d.ts +49 -23
  8. package/dist/ai-promise-db.d.ts.map +1 -1
  9. package/dist/ai-promise-db.js +91 -63
  10. package/dist/ai-promise-db.js.map +1 -1
  11. package/dist/authorization.d.ts.map +1 -1
  12. package/dist/authorization.js +38 -30
  13. package/dist/authorization.js.map +1 -1
  14. package/dist/cascade-orchestrator.d.ts +404 -0
  15. package/dist/cascade-orchestrator.d.ts.map +1 -0
  16. package/dist/cascade-orchestrator.js +828 -0
  17. package/dist/cascade-orchestrator.js.map +1 -0
  18. package/dist/cascade-write-strategy.d.ts +584 -0
  19. package/dist/cascade-write-strategy.d.ts.map +1 -0
  20. package/dist/cascade-write-strategy.js +590 -0
  21. package/dist/cascade-write-strategy.js.map +1 -0
  22. package/dist/ch-adapter.d.ts +358 -0
  23. package/dist/ch-adapter.d.ts.map +1 -0
  24. package/dist/ch-adapter.js +929 -0
  25. package/dist/ch-adapter.js.map +1 -0
  26. package/dist/client/index.d.ts +42 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +43 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client.d.ts +266 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +81 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/constants.d.ts +64 -1
  35. package/dist/constants.d.ts.map +1 -1
  36. package/dist/constants.js +52 -2
  37. package/dist/constants.js.map +1 -1
  38. package/dist/dataloader.d.ts +99 -0
  39. package/dist/dataloader.d.ts.map +1 -0
  40. package/dist/dataloader.js +225 -0
  41. package/dist/dataloader.js.map +1 -0
  42. package/dist/db-provider-port.d.ts +501 -0
  43. package/dist/db-provider-port.d.ts.map +1 -0
  44. package/dist/db-provider-port.js +113 -0
  45. package/dist/db-provider-port.js.map +1 -0
  46. package/dist/digital-objects-provider.d.ts +49 -0
  47. package/dist/digital-objects-provider.d.ts.map +1 -0
  48. package/dist/digital-objects-provider.js +55 -0
  49. package/dist/digital-objects-provider.js.map +1 -0
  50. package/dist/do-sqlite-adapter.d.ts +402 -0
  51. package/dist/do-sqlite-adapter.d.ts.map +1 -0
  52. package/dist/do-sqlite-adapter.js +745 -0
  53. package/dist/do-sqlite-adapter.js.map +1 -0
  54. package/dist/docs-rels/custom-types.d.ts +134 -0
  55. package/dist/docs-rels/custom-types.d.ts.map +1 -0
  56. package/dist/docs-rels/custom-types.js +70 -0
  57. package/dist/docs-rels/custom-types.js.map +1 -0
  58. package/dist/docs-rels/index.d.ts +16 -0
  59. package/dist/docs-rels/index.d.ts.map +1 -0
  60. package/dist/docs-rels/index.js +16 -0
  61. package/dist/docs-rels/index.js.map +1 -0
  62. package/dist/docs-rels/migrations/index.d.ts +30 -0
  63. package/dist/docs-rels/migrations/index.d.ts.map +1 -0
  64. package/dist/docs-rels/migrations/index.js +128 -0
  65. package/dist/docs-rels/migrations/index.js.map +1 -0
  66. package/dist/docs-rels/schema.d.ts +2961 -0
  67. package/dist/docs-rels/schema.d.ts.map +1 -0
  68. package/dist/docs-rels/schema.js +244 -0
  69. package/dist/docs-rels/schema.js.map +1 -0
  70. package/dist/durable-clickhouse.d.ts.map +1 -1
  71. package/dist/durable-clickhouse.js +16 -13
  72. package/dist/durable-clickhouse.js.map +1 -1
  73. package/dist/durable-promise.d.ts.map +1 -1
  74. package/dist/durable-promise.js +34 -15
  75. package/dist/durable-promise.js.map +1 -1
  76. package/dist/errors.d.ts +127 -0
  77. package/dist/errors.d.ts.map +1 -0
  78. package/dist/errors.js +210 -0
  79. package/dist/errors.js.map +1 -0
  80. package/dist/eventbridge.d.ts +117 -0
  81. package/dist/eventbridge.d.ts.map +1 -0
  82. package/dist/eventbridge.js +238 -0
  83. package/dist/eventbridge.js.map +1 -0
  84. package/dist/events.d.ts +2 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +1 -1
  87. package/dist/events.js.map +1 -1
  88. package/dist/execution-queue.d.ts.map +1 -1
  89. package/dist/execution-queue.js +4 -5
  90. package/dist/execution-queue.js.map +1 -1
  91. package/dist/index.d.ts +35 -8
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +106 -6
  94. package/dist/index.js.map +1 -1
  95. package/dist/linguistic.d.ts +3 -108
  96. package/dist/linguistic.d.ts.map +1 -1
  97. package/dist/linguistic.js +3 -372
  98. package/dist/linguistic.js.map +1 -1
  99. package/dist/logger.d.ts +132 -0
  100. package/dist/logger.d.ts.map +1 -0
  101. package/dist/logger.js +137 -0
  102. package/dist/logger.js.map +1 -0
  103. package/dist/memory-provider.d.ts +128 -0
  104. package/dist/memory-provider.d.ts.map +1 -1
  105. package/dist/memory-provider.js +592 -257
  106. package/dist/memory-provider.js.map +1 -1
  107. package/dist/pg-adapter.d.ts +424 -0
  108. package/dist/pg-adapter.d.ts.map +1 -0
  109. package/dist/pg-adapter.js +921 -0
  110. package/dist/pg-adapter.js.map +1 -0
  111. package/dist/pipelines-iceberg-emitter.d.ts +327 -0
  112. package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
  113. package/dist/pipelines-iceberg-emitter.js +351 -0
  114. package/dist/pipelines-iceberg-emitter.js.map +1 -0
  115. package/dist/provider-capabilities.d.ts +146 -0
  116. package/dist/provider-capabilities.d.ts.map +1 -0
  117. package/dist/provider-capabilities.js +214 -0
  118. package/dist/provider-capabilities.js.map +1 -0
  119. package/dist/rdb-provider-adapter.d.ts +195 -0
  120. package/dist/rdb-provider-adapter.d.ts.map +1 -0
  121. package/dist/rdb-provider-adapter.js +291 -0
  122. package/dist/rdb-provider-adapter.js.map +1 -0
  123. package/dist/schema/cascade.d.ts +48 -17
  124. package/dist/schema/cascade.d.ts.map +1 -1
  125. package/dist/schema/cascade.js +477 -278
  126. package/dist/schema/cascade.js.map +1 -1
  127. package/dist/schema/definition-caches.d.ts +24 -0
  128. package/dist/schema/definition-caches.d.ts.map +1 -0
  129. package/dist/schema/definition-caches.js +26 -0
  130. package/dist/schema/definition-caches.js.map +1 -0
  131. package/dist/schema/dependency-graph.d.ts +21 -109
  132. package/dist/schema/dependency-graph.d.ts.map +1 -1
  133. package/dist/schema/dependency-graph.js +25 -333
  134. package/dist/schema/dependency-graph.js.map +1 -1
  135. package/dist/schema/diff.d.ts +103 -0
  136. package/dist/schema/diff.d.ts.map +1 -0
  137. package/dist/schema/diff.js +329 -0
  138. package/dist/schema/diff.js.map +1 -0
  139. package/dist/schema/entity-operations.d.ts +99 -0
  140. package/dist/schema/entity-operations.d.ts.map +1 -0
  141. package/dist/schema/entity-operations.js +818 -0
  142. package/dist/schema/entity-operations.js.map +1 -0
  143. package/dist/schema/index.d.ts +28 -34
  144. package/dist/schema/index.d.ts.map +1 -1
  145. package/dist/schema/index.js +454 -521
  146. package/dist/schema/index.js.map +1 -1
  147. package/dist/schema/migration.d.ts +205 -0
  148. package/dist/schema/migration.d.ts.map +1 -0
  149. package/dist/schema/migration.js +327 -0
  150. package/dist/schema/migration.js.map +1 -0
  151. package/dist/schema/nl-query-generator.d.ts +68 -0
  152. package/dist/schema/nl-query-generator.d.ts.map +1 -0
  153. package/dist/schema/nl-query-generator.js +362 -0
  154. package/dist/schema/nl-query-generator.js.map +1 -0
  155. package/dist/schema/nl-query.d.ts +65 -0
  156. package/dist/schema/nl-query.d.ts.map +1 -0
  157. package/dist/schema/nl-query.js +178 -0
  158. package/dist/schema/nl-query.js.map +1 -0
  159. package/dist/schema/parse.d.ts.map +1 -1
  160. package/dist/schema/parse.js +144 -89
  161. package/dist/schema/parse.js.map +1 -1
  162. package/dist/schema/provider.d.ts +37 -0
  163. package/dist/schema/provider.d.ts.map +1 -1
  164. package/dist/schema/provider.js +15 -7
  165. package/dist/schema/provider.js.map +1 -1
  166. package/dist/schema/resolve.d.ts +46 -5
  167. package/dist/schema/resolve.d.ts.map +1 -1
  168. package/dist/schema/resolve.js +237 -95
  169. package/dist/schema/resolve.js.map +1 -1
  170. package/dist/schema/search-utils.d.ts +76 -0
  171. package/dist/schema/search-utils.d.ts.map +1 -0
  172. package/dist/schema/search-utils.js +86 -0
  173. package/dist/schema/search-utils.js.map +1 -0
  174. package/dist/schema/seed.d.ts +53 -0
  175. package/dist/schema/seed.d.ts.map +1 -0
  176. package/dist/schema/seed.js +94 -0
  177. package/dist/schema/seed.js.map +1 -0
  178. package/dist/schema/semantic.d.ts +10 -0
  179. package/dist/schema/semantic.d.ts.map +1 -1
  180. package/dist/schema/semantic.js +192 -86
  181. package/dist/schema/semantic.js.map +1 -1
  182. package/dist/schema/sub-apis.d.ts +52 -0
  183. package/dist/schema/sub-apis.d.ts.map +1 -0
  184. package/dist/schema/sub-apis.js +216 -0
  185. package/dist/schema/sub-apis.js.map +1 -0
  186. package/dist/schema/system-entities.d.ts +42 -0
  187. package/dist/schema/system-entities.d.ts.map +1 -0
  188. package/dist/schema/system-entities.js +101 -0
  189. package/dist/schema/system-entities.js.map +1 -0
  190. package/dist/schema/types.d.ts +91 -9
  191. package/dist/schema/types.d.ts.map +1 -1
  192. package/dist/schema/union-fallback.d.ts.map +1 -1
  193. package/dist/schema/union-fallback.js +21 -15
  194. package/dist/schema/union-fallback.js.map +1 -1
  195. package/dist/schema/value-generators/ai.d.ts +54 -0
  196. package/dist/schema/value-generators/ai.d.ts.map +1 -0
  197. package/dist/schema/value-generators/ai.js +136 -0
  198. package/dist/schema/value-generators/ai.js.map +1 -0
  199. package/dist/schema/value-generators/index.d.ts +126 -0
  200. package/dist/schema/value-generators/index.d.ts.map +1 -0
  201. package/dist/schema/value-generators/index.js +219 -0
  202. package/dist/schema/value-generators/index.js.map +1 -0
  203. package/dist/schema/value-generators/placeholder.d.ts +52 -0
  204. package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
  205. package/dist/schema/value-generators/placeholder.js +328 -0
  206. package/dist/schema/value-generators/placeholder.js.map +1 -0
  207. package/dist/schema/value-generators/types.d.ts +116 -0
  208. package/dist/schema/value-generators/types.d.ts.map +1 -0
  209. package/dist/schema/value-generators/types.js +11 -0
  210. package/dist/schema/value-generators/types.js.map +1 -0
  211. package/dist/schema/version.d.ts +111 -0
  212. package/dist/schema/version.d.ts.map +1 -0
  213. package/dist/schema/version.js +190 -0
  214. package/dist/schema/version.js.map +1 -0
  215. package/dist/schema.d.ts +1095 -24
  216. package/dist/schema.d.ts.map +1 -1
  217. package/dist/schema.js +2852 -40
  218. package/dist/schema.js.map +1 -1
  219. package/dist/semantic-vectors.d.ts +39 -0
  220. package/dist/semantic-vectors.d.ts.map +1 -0
  221. package/dist/semantic-vectors.js +334 -0
  222. package/dist/semantic-vectors.js.map +1 -0
  223. package/dist/semantic.d.ts +29 -1
  224. package/dist/semantic.d.ts.map +1 -1
  225. package/dist/semantic.js +26 -16
  226. package/dist/semantic.js.map +1 -1
  227. package/dist/telemetry.d.ts +128 -0
  228. package/dist/telemetry.d.ts.map +1 -0
  229. package/dist/telemetry.js +305 -0
  230. package/dist/telemetry.js.map +1 -0
  231. package/dist/tests.d.ts.map +1 -1
  232. package/dist/tests.js +30 -22
  233. package/dist/tests.js.map +1 -1
  234. package/dist/type-guards.d.ts +50 -5
  235. package/dist/type-guards.d.ts.map +1 -1
  236. package/dist/type-guards.js +87 -16
  237. package/dist/type-guards.js.map +1 -1
  238. package/dist/types.d.ts +33 -245
  239. package/dist/types.d.ts.map +1 -1
  240. package/dist/types.js +62 -72
  241. package/dist/types.js.map +1 -1
  242. package/dist/validation.d.ts +2 -5
  243. package/dist/validation.d.ts.map +1 -1
  244. package/dist/validation.js +65 -93
  245. package/dist/validation.js.map +1 -1
  246. package/dist/worker/db-provider.d.ts +168 -0
  247. package/dist/worker/db-provider.d.ts.map +1 -0
  248. package/dist/worker/db-provider.js +277 -0
  249. package/dist/worker/db-provider.js.map +1 -0
  250. package/dist/worker/index.d.ts +35 -0
  251. package/dist/worker/index.d.ts.map +1 -0
  252. package/dist/worker/index.js +37 -0
  253. package/dist/worker/index.js.map +1 -0
  254. package/dist/worker.d.ts +779 -0
  255. package/dist/worker.d.ts.map +1 -0
  256. package/dist/worker.js +2786 -0
  257. package/dist/worker.js.map +1 -0
  258. package/package.json +46 -16
  259. package/src/docs-rels/migrations/0001-init.sql +125 -0
  260. package/LICENSE +0 -21
package/dist/worker.js ADDED
@@ -0,0 +1,2786 @@
1
+ /**
2
+ * Worker Export - WorkerEntrypoint for RPC access to AI Database
3
+ *
4
+ * Exposes database operations via Cloudflare RPC.
5
+ * Works both in Cloudflare Workers and standalone (with MemoryProvider).
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // wrangler.jsonc
10
+ * {
11
+ * "services": [
12
+ * { "binding": "AI_DATABASE", "service": "ai-database" }
13
+ * ]
14
+ * }
15
+ *
16
+ * // worker.ts - consuming service
17
+ * export default {
18
+ * async fetch(request: Request, env: Env) {
19
+ * const db = env.AI_DATABASE.connect('my-namespace')
20
+ * const post = await db.create('Post', { title: 'Hello' })
21
+ * return Response.json(post)
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+ // ===========================================================================
29
+ // Mock classes for non-Cloudflare environments
30
+ // ===========================================================================
31
+ class MockRpcTarget {
32
+ }
33
+ class MockWorkerEntrypoint {
34
+ env;
35
+ ctx;
36
+ }
37
+ class MockDurableObject {
38
+ ctx;
39
+ env;
40
+ constructor(_state, _env) { }
41
+ }
42
+ // Try to import from cloudflare:workers, fall back to mocks
43
+ let WorkerEntrypoint;
44
+ let RpcTarget;
45
+ let DurableObjectBase;
46
+ try {
47
+ // @ts-expect-error - cloudflare:workers is only available in Cloudflare Workers runtime
48
+ const cfWorkers = await import('cloudflare:workers');
49
+ WorkerEntrypoint = cfWorkers.WorkerEntrypoint;
50
+ RpcTarget = cfWorkers.RpcTarget;
51
+ DurableObjectBase = cfWorkers.DurableObject;
52
+ }
53
+ catch {
54
+ WorkerEntrypoint = MockWorkerEntrypoint;
55
+ RpcTarget = MockRpcTarget;
56
+ DurableObjectBase = MockDurableObject;
57
+ }
58
+ import { MemoryProvider } from './memory-provider.js';
59
+ /**
60
+ * Global namespace registry for in-memory providers (used when no DO binding is available)
61
+ * This enables namespace isolation and persistence across connect() calls in tests
62
+ */
63
+ const namespaceProviders = new Map();
64
+ /**
65
+ * Get or create a MemoryProvider for a namespace
66
+ */
67
+ function getOrCreateProvider(namespace, options) {
68
+ let provider = namespaceProviders.get(namespace);
69
+ if (!provider) {
70
+ provider = new MemoryProvider(options);
71
+ namespaceProviders.set(namespace, provider);
72
+ }
73
+ return provider;
74
+ }
75
+ /**
76
+ * DatabaseServiceCore - RpcTarget wrapper around MemoryProvider
77
+ *
78
+ * Exposes all required methods as RPC-callable methods.
79
+ * This is the core service class that can be instantiated directly.
80
+ */
81
+ export class DatabaseServiceCore extends RpcTarget {
82
+ provider;
83
+ constructor(namespace = 'default', options) {
84
+ super();
85
+ this.provider = getOrCreateProvider(namespace, options);
86
+ }
87
+ // ===========================================================================
88
+ // Configuration
89
+ // ===========================================================================
90
+ /**
91
+ * Set embeddings configuration for auto-generation
92
+ */
93
+ setEmbeddingsConfig(config) {
94
+ this.provider.setEmbeddingsConfig(config);
95
+ }
96
+ /**
97
+ * Enable or disable ai-functions for embeddings
98
+ */
99
+ setUseAiFunctions(enabled) {
100
+ this.provider.setUseAiFunctions(enabled);
101
+ }
102
+ /**
103
+ * Set a custom embedding provider
104
+ */
105
+ setEmbeddingProvider(provider) {
106
+ this.provider.setEmbeddingProvider(provider);
107
+ }
108
+ // ===========================================================================
109
+ // CRUD Operations
110
+ // ===========================================================================
111
+ /**
112
+ * Get an entity by type and ID
113
+ */
114
+ async get(type, id) {
115
+ const result = await this.provider.get(type, id);
116
+ if (!result)
117
+ return null;
118
+ return { $id: id, $type: type, ...result };
119
+ }
120
+ /**
121
+ * List entities by type
122
+ */
123
+ async list(type, options) {
124
+ const results = await this.provider.list(type, options);
125
+ return results.map((r) => ({ $type: type, ...r }));
126
+ }
127
+ /**
128
+ * Create an entity
129
+ */
130
+ async create(type, data, id) {
131
+ const result = await this.provider.create(type, id, data);
132
+ return { $type: type, ...result };
133
+ }
134
+ /**
135
+ * Update an entity
136
+ */
137
+ async update(type, id, data) {
138
+ const result = await this.provider.update(type, id, data);
139
+ return { $id: id, $type: type, ...result };
140
+ }
141
+ /**
142
+ * Delete an entity
143
+ */
144
+ async delete(type, id) {
145
+ return this.provider.delete(type, id);
146
+ }
147
+ // ===========================================================================
148
+ // Search Operations
149
+ // ===========================================================================
150
+ /**
151
+ * Full-text search
152
+ */
153
+ async search(type, query, options) {
154
+ const results = await this.provider.search(type, query, options);
155
+ return results.map((r) => ({ $type: type, ...r }));
156
+ }
157
+ /**
158
+ * Semantic search using vector similarity
159
+ */
160
+ async semanticSearch(type, query, options) {
161
+ const provider = this.provider;
162
+ // Check if provider supports semantic search
163
+ if (provider.semanticSearch) {
164
+ const results = await provider.semanticSearch(type, query, options);
165
+ return results;
166
+ }
167
+ // Fallback to regular search
168
+ const results = await provider.search(type, query, options);
169
+ return results.map((r, i) => ({
170
+ $type: type,
171
+ $score: 1 - i * 0.1, // Decreasing score based on position
172
+ ...r,
173
+ }));
174
+ }
175
+ /**
176
+ * Hybrid search combining FTS and semantic
177
+ */
178
+ async hybridSearch(type, query, options) {
179
+ const provider = this.provider;
180
+ // Check if provider supports hybrid search
181
+ if (provider.hybridSearch) {
182
+ const results = await provider.hybridSearch(type, query, options);
183
+ return results;
184
+ }
185
+ // Fallback to semantic search
186
+ const results = await this.semanticSearch(type, query, options);
187
+ return results.map((r, i) => ({
188
+ ...r,
189
+ $rrfScore: r.$score,
190
+ $ftsRank: i + 1,
191
+ $semanticRank: i + 1,
192
+ }));
193
+ }
194
+ // ===========================================================================
195
+ // Relationship Operations
196
+ // ===========================================================================
197
+ /**
198
+ * Get related entities
199
+ */
200
+ async related(type, id, relation) {
201
+ const results = await this.provider.related(type, id, relation);
202
+ return results.map((r) => r);
203
+ }
204
+ /**
205
+ * Create a relationship between two entities
206
+ */
207
+ async relate(fromType, fromId, relation, toType, toId, metadata) {
208
+ return this.provider.relate(fromType, fromId, relation, toType, toId, metadata);
209
+ }
210
+ /**
211
+ * Remove a relationship between two entities
212
+ */
213
+ async unrelate(fromType, fromId, relation, toType, toId) {
214
+ return this.provider.unrelate(fromType, fromId, relation, toType, toId);
215
+ }
216
+ // ===========================================================================
217
+ // Events API
218
+ // ===========================================================================
219
+ /**
220
+ * Subscribe to events matching a pattern
221
+ * @returns Unsubscribe function ID (use unsubscribe() to remove)
222
+ */
223
+ on(pattern, handler) {
224
+ if ('on' in this.provider) {
225
+ const providerWithOn = this.provider;
226
+ const unsubscribe = providerWithOn.on(pattern, handler);
227
+ // Store unsubscribe function with unique ID
228
+ const handlerId = crypto.randomUUID();
229
+ this._eventHandlers.set(handlerId, unsubscribe);
230
+ return handlerId;
231
+ }
232
+ return '';
233
+ }
234
+ _eventHandlers = new Map();
235
+ /**
236
+ * Unsubscribe from events
237
+ */
238
+ unsubscribe(handlerId) {
239
+ const unsubscribe = this._eventHandlers.get(handlerId);
240
+ if (unsubscribe) {
241
+ unsubscribe();
242
+ this._eventHandlers.delete(handlerId);
243
+ }
244
+ }
245
+ /**
246
+ * Emit an event
247
+ */
248
+ async emit(eventOrOptions, data) {
249
+ if ('emit' in this.provider) {
250
+ const providerWithEmit = this.provider;
251
+ if (typeof eventOrOptions === 'string') {
252
+ return providerWithEmit.emit(eventOrOptions, data);
253
+ }
254
+ return providerWithEmit.emit(eventOrOptions);
255
+ }
256
+ return null;
257
+ }
258
+ /**
259
+ * List events
260
+ */
261
+ async listEvents(options) {
262
+ if ('listEvents' in this.provider) {
263
+ const providerWithListEvents = this.provider;
264
+ return providerWithListEvents.listEvents(options);
265
+ }
266
+ return [];
267
+ }
268
+ // ===========================================================================
269
+ // Actions API
270
+ // ===========================================================================
271
+ /**
272
+ * Create a new action
273
+ */
274
+ async createAction(options) {
275
+ if ('createAction' in this.provider) {
276
+ const providerWithCreateAction = this.provider;
277
+ return providerWithCreateAction.createAction(options);
278
+ }
279
+ return null;
280
+ }
281
+ /**
282
+ * Get an action by ID
283
+ */
284
+ async getAction(id) {
285
+ if ('getAction' in this.provider) {
286
+ const providerWithGetAction = this.provider;
287
+ return providerWithGetAction.getAction(id);
288
+ }
289
+ return null;
290
+ }
291
+ /**
292
+ * Update an action
293
+ */
294
+ async updateAction(id, updates) {
295
+ if ('updateAction' in this.provider) {
296
+ const providerWithUpdateAction = this.provider;
297
+ return providerWithUpdateAction.updateAction(id, updates);
298
+ }
299
+ return null;
300
+ }
301
+ /**
302
+ * List actions
303
+ */
304
+ async listActions(options) {
305
+ if ('listActions' in this.provider) {
306
+ const providerWithListActions = this.provider;
307
+ return providerWithListActions.listActions(options);
308
+ }
309
+ return [];
310
+ }
311
+ // ===========================================================================
312
+ // Artifacts API
313
+ // ===========================================================================
314
+ /**
315
+ * Get an artifact
316
+ */
317
+ async getArtifact(url, type) {
318
+ if ('getArtifact' in this.provider) {
319
+ const providerWithGetArtifact = this.provider;
320
+ return providerWithGetArtifact.getArtifact(url, type);
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * Set an artifact
326
+ */
327
+ async setArtifact(url, type, data) {
328
+ if ('setArtifact' in this.provider) {
329
+ const providerWithSetArtifact = this.provider;
330
+ return providerWithSetArtifact.setArtifact(url, type, data);
331
+ }
332
+ }
333
+ /**
334
+ * Delete an artifact
335
+ */
336
+ async deleteArtifact(url, type) {
337
+ if ('deleteArtifact' in this.provider) {
338
+ const providerWithDeleteArtifact = this.provider;
339
+ return providerWithDeleteArtifact.deleteArtifact(url, type);
340
+ }
341
+ }
342
+ /**
343
+ * List artifacts for a URL
344
+ */
345
+ async listArtifacts(url) {
346
+ if ('listArtifacts' in this.provider) {
347
+ const providerWithListArtifacts = this.provider;
348
+ return providerWithListArtifacts.listArtifacts(url);
349
+ }
350
+ return [];
351
+ }
352
+ // ===========================================================================
353
+ // Utility Methods
354
+ // ===========================================================================
355
+ /**
356
+ * Clear all data in the provider (useful for testing)
357
+ */
358
+ clear() {
359
+ if ('clear' in this.provider) {
360
+ const providerWithClear = this.provider;
361
+ providerWithClear.clear();
362
+ }
363
+ }
364
+ }
365
+ // =============================================================================
366
+ // DatabaseDO - Durable Object with SQLite storage for core _data and _rels tables
367
+ // =============================================================================
368
+ /**
369
+ * DatabaseDO - Durable Object using SQLite for the core schema layer.
370
+ *
371
+ * Provides two tables:
372
+ * - `_data`: id TEXT PRIMARY KEY, type TEXT, data TEXT (JSON), created_at TEXT, updated_at TEXT
373
+ * - `_rels`: from_id TEXT, relation TEXT, to_id TEXT, metadata TEXT (JSON),
374
+ * PRIMARY KEY(from_id, relation, to_id)
375
+ *
376
+ * Handles HTTP requests for CRUD operations on data records and relationships,
377
+ * plus graph traversal queries.
378
+ */
379
+ export class DatabaseDO extends DurableObjectBase {
380
+ sql;
381
+ initialized = false;
382
+ // Pipeline state for R2 streaming
383
+ pipelineBuffer = [];
384
+ pipelineConfig = { retryEnabled: false, batchSize: 100 };
385
+ pipelineStats = { eventsProcessed: 0, batchesSent: 0 };
386
+ // Embeddings configuration
387
+ embeddingsConfig = { model: '@cf/baai/bge-base-en-v1.5' };
388
+ embeddingsCacheStats = { cacheHits: 0, cacheMisses: 0 };
389
+ batchJobs = new Map();
390
+ constructor(state, env) {
391
+ super(state, env);
392
+ this.sql = state.storage.sql;
393
+ }
394
+ ensureSchema() {
395
+ if (this.initialized)
396
+ return;
397
+ // Create _data table
398
+ this.sql.exec(`
399
+ CREATE TABLE IF NOT EXISTS _data (
400
+ id TEXT PRIMARY KEY,
401
+ type TEXT NOT NULL,
402
+ data TEXT NOT NULL DEFAULT '{}',
403
+ created_at TEXT NOT NULL,
404
+ updated_at TEXT NOT NULL
405
+ )
406
+ `);
407
+ // Create _rels table with created_at
408
+ this.sql.exec(`
409
+ CREATE TABLE IF NOT EXISTS _rels (
410
+ from_id TEXT NOT NULL,
411
+ relation TEXT NOT NULL,
412
+ to_id TEXT NOT NULL,
413
+ metadata TEXT,
414
+ created_at TEXT NOT NULL,
415
+ PRIMARY KEY(from_id, relation, to_id)
416
+ )
417
+ `);
418
+ // Create _meta table for schema versioning
419
+ this.sql.exec(`
420
+ CREATE TABLE IF NOT EXISTS _meta (
421
+ key TEXT PRIMARY KEY,
422
+ value TEXT NOT NULL
423
+ )
424
+ `);
425
+ // Set initial schema version
426
+ this.sql.exec(`
427
+ INSERT OR IGNORE INTO _meta (key, value) VALUES ('version', '1')
428
+ `);
429
+ // Create indexes for performance
430
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_data_type ON _data(type)`);
431
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_data_created_at ON _data(created_at)`);
432
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_rels_from_id_relation ON _rels(from_id, relation)`);
433
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_rels_to_id_relation ON _rels(to_id, relation)`);
434
+ // Create _events table for event sourcing
435
+ this.sql.exec(`
436
+ CREATE TABLE IF NOT EXISTS _events (
437
+ id TEXT PRIMARY KEY,
438
+ event TEXT NOT NULL,
439
+ actor TEXT,
440
+ object TEXT,
441
+ data TEXT,
442
+ result TEXT,
443
+ previous_data TEXT,
444
+ timestamp TEXT NOT NULL
445
+ )
446
+ `);
447
+ // Create indexes for _events table
448
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_events_event ON _events(event)`);
449
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_events_ts ON _events(timestamp)`);
450
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_events_obj ON _events(object)`);
451
+ // Create _subscriptions table for event subscriptions
452
+ this.sql.exec(`
453
+ CREATE TABLE IF NOT EXISTS _subscriptions (
454
+ id TEXT PRIMARY KEY,
455
+ pattern TEXT NOT NULL,
456
+ webhook TEXT NOT NULL,
457
+ created_at TEXT NOT NULL
458
+ )
459
+ `);
460
+ // Create _embeddings table for semantic search
461
+ this.sql.exec(`
462
+ CREATE TABLE IF NOT EXISTS _embeddings (
463
+ id TEXT PRIMARY KEY,
464
+ entity_type TEXT NOT NULL,
465
+ entity_id TEXT NOT NULL,
466
+ model TEXT NOT NULL,
467
+ vector TEXT NOT NULL,
468
+ content_hash TEXT,
469
+ created_at TEXT NOT NULL,
470
+ updated_at TEXT NOT NULL,
471
+ UNIQUE(entity_type, entity_id)
472
+ )
473
+ `);
474
+ // Create index for _embeddings table
475
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_embeddings_entity ON _embeddings(entity_type, entity_id)`);
476
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_embeddings_type ON _embeddings(entity_type)`);
477
+ this.initialized = true;
478
+ }
479
+ async fetch(request) {
480
+ this.ensureSchema();
481
+ const url = new URL(request.url);
482
+ const path = url.pathname;
483
+ const method = request.method;
484
+ try {
485
+ // Route: /data (list or insert)
486
+ if (path === '/data' && method === 'GET') {
487
+ return this.handleListData(url);
488
+ }
489
+ if (path === '/data' && method === 'POST') {
490
+ return this.handleInsertData(request);
491
+ }
492
+ // Route: /data/:id (get, update, delete)
493
+ const dataMatch = path.match(/^\/data\/(.+)$/);
494
+ if (dataMatch) {
495
+ const id = decodeURIComponent(dataMatch[1]);
496
+ if (method === 'GET')
497
+ return this.handleGetData(id);
498
+ if (method === 'PATCH')
499
+ return this.handleUpdateData(id, request);
500
+ if (method === 'DELETE')
501
+ return this.handleDeleteData(id, url, request);
502
+ }
503
+ // Route: /rels (list or create)
504
+ if (path === '/rels' && method === 'GET') {
505
+ return this.handleQueryRels(url);
506
+ }
507
+ if (path === '/rels' && method === 'POST') {
508
+ return this.handleCreateRel(request);
509
+ }
510
+ // Route: /rels/delete (delete relationship)
511
+ if (path === '/rels/delete' && method === 'DELETE') {
512
+ return this.handleDeleteRel(request);
513
+ }
514
+ // Route: /traverse (graph traversal)
515
+ if (path === '/traverse' && method === 'GET') {
516
+ return this.handleTraverse(url);
517
+ }
518
+ if (path === '/traverse/filter' && method === 'POST') {
519
+ return this.handleTraverseFilter(request);
520
+ }
521
+ // Route: /rels with PATCH for updating metadata
522
+ if (path === '/rels' && method === 'PATCH') {
523
+ return this.handleUpdateRel(request);
524
+ }
525
+ // Route: /meta/indexes (list indexes)
526
+ if (path === '/meta/indexes' && method === 'GET') {
527
+ return this.handleGetIndexes();
528
+ }
529
+ // Route: /meta/version (schema version)
530
+ if (path === '/meta/version' && method === 'GET') {
531
+ return this.handleGetVersion();
532
+ }
533
+ // Route: /query/list (GET for simple, POST for complex)
534
+ if (path === '/query/list' && method === 'GET') {
535
+ return this.handleQueryList(url);
536
+ }
537
+ if (path === '/query/list' && method === 'POST') {
538
+ return this.handleQueryListPost(request);
539
+ }
540
+ // Route: /query/find (POST)
541
+ if (path === '/query/find' && method === 'POST') {
542
+ return this.handleQueryFind(request);
543
+ }
544
+ // Route: /query/search (POST)
545
+ if (path === '/query/search' && method === 'POST') {
546
+ return this.handleQuerySearch(request);
547
+ }
548
+ // Route: /events (list or create custom events)
549
+ if (path === '/events' && method === 'GET') {
550
+ return this.handleListEvents(url);
551
+ }
552
+ if (path === '/events' && method === 'POST') {
553
+ return this.handleCreateEvent(request);
554
+ }
555
+ // Route: /events/replay (replay events)
556
+ if (path === '/events/replay' && method === 'POST') {
557
+ return this.handleReplayEvents(request);
558
+ }
559
+ // Route: /events/rebuild (rebuild entity from events)
560
+ if (path === '/events/rebuild' && method === 'POST') {
561
+ return this.handleRebuildEntity(request);
562
+ }
563
+ // Route: /events/subscribe (create subscription)
564
+ if (path === '/events/subscribe' && method === 'POST') {
565
+ return this.handleSubscribe(request);
566
+ }
567
+ // Route: /events/subscriptions (list subscriptions)
568
+ if (path === '/events/subscriptions' && method === 'GET') {
569
+ return this.handleListSubscriptions();
570
+ }
571
+ // Route: /events/subscriptions/:id (delete subscription)
572
+ const subMatch = path.match(/^\/events\/subscriptions\/([^/]+)$/);
573
+ if (subMatch) {
574
+ const subId = decodeURIComponent(subMatch[1]);
575
+ if (method === 'DELETE')
576
+ return this.handleUnsubscribe(subId);
577
+ }
578
+ // Route: /events/subscriptions/:id/deliveries (list deliveries)
579
+ const deliveriesMatch = path.match(/^\/events\/subscriptions\/([^/]+)\/deliveries$/);
580
+ if (deliveriesMatch && method === 'GET') {
581
+ const subId = decodeURIComponent(deliveriesMatch[1]);
582
+ return this.handleListDeliveries(subId);
583
+ }
584
+ // Route: /pipeline/status (pipeline status)
585
+ if (path === '/pipeline/status' && method === 'GET') {
586
+ return this.handlePipelineStatus();
587
+ }
588
+ // Route: /pipeline/flush (flush pipeline)
589
+ if (path === '/pipeline/flush' && method === 'POST') {
590
+ return this.handlePipelineFlush();
591
+ }
592
+ // Route: /pipeline/r2/list (list R2 objects)
593
+ if (path === '/pipeline/r2/list' && method === 'GET') {
594
+ return this.handlePipelineR2List();
595
+ }
596
+ // Route: /pipeline/config (configure pipeline)
597
+ if (path === '/pipeline/config' && method === 'POST') {
598
+ return this.handlePipelineConfig(request);
599
+ }
600
+ // Route: /pipeline/test-error (test error handling)
601
+ if (path === '/pipeline/test-error' && method === 'POST') {
602
+ return this.handlePipelineTestError(request);
603
+ }
604
+ // ===========================================================================
605
+ // Semantic Search Routes
606
+ // ===========================================================================
607
+ // Route: /config/embeddings (configure embedding model)
608
+ if (path === '/config/embeddings' && method === 'POST') {
609
+ return this.handleConfigureEmbeddings(request);
610
+ }
611
+ // Route: /embeddings (list all embeddings)
612
+ if (path === '/embeddings' && method === 'GET') {
613
+ return this.handleListEmbeddings(url);
614
+ }
615
+ // Route: /embeddings/stats (get cache stats)
616
+ if (path === '/embeddings/stats' && method === 'GET') {
617
+ return this.handleEmbeddingsStats();
618
+ }
619
+ // Route: /embeddings/warmup (warm up embedding cache)
620
+ if (path === '/embeddings/warmup' && method === 'POST') {
621
+ return this.handleEmbeddingsWarmup(request);
622
+ }
623
+ // Route: /embeddings/generate (generate embeddings for all entities of a type)
624
+ if (path === '/embeddings/generate' && method === 'POST') {
625
+ return this.handleEmbeddingsGenerate(request);
626
+ }
627
+ // Route: /embeddings/batch (batch process embeddings)
628
+ if (path === '/embeddings/batch' && method === 'POST') {
629
+ return this.handleEmbeddingsBatch(request);
630
+ }
631
+ // Route: /embeddings/batch/start (start batch job)
632
+ if (path === '/embeddings/batch/start' && method === 'POST') {
633
+ return this.handleEmbeddingsBatchStart(request);
634
+ }
635
+ // Route: /embeddings/batch/:jobId/status (batch job status)
636
+ const batchStatusMatch = path.match(/^\/embeddings\/batch\/([^/]+)\/status$/);
637
+ if (batchStatusMatch && method === 'GET') {
638
+ const jobId = decodeURIComponent(batchStatusMatch[1]);
639
+ return this.handleEmbeddingsBatchStatus(jobId);
640
+ }
641
+ // Route: /embeddings/:type/:id (get or generate embedding for an entity)
642
+ const embeddingMatch = path.match(/^\/embeddings\/([^/]+)\/([^/]+)$/);
643
+ if (embeddingMatch && method === 'GET') {
644
+ const entityType = decodeURIComponent(embeddingMatch[1]);
645
+ const entityId = decodeURIComponent(embeddingMatch[2]);
646
+ return this.handleGetEmbedding(entityType, entityId);
647
+ }
648
+ // Route: /search/semantic (semantic search)
649
+ if (path === '/search/semantic' && method === 'POST') {
650
+ return this.handleSemanticSearch(request);
651
+ }
652
+ // Route: /search/hybrid (hybrid FTS + semantic search)
653
+ if (path === '/search/hybrid' && method === 'POST') {
654
+ return this.handleHybridSearch(request);
655
+ }
656
+ return Response.json({ error: 'Not found' }, { status: 404 });
657
+ }
658
+ catch (err) {
659
+ const message = err instanceof Error ? err.message : 'Internal error';
660
+ return Response.json({ error: message }, { status: 500 });
661
+ }
662
+ }
663
+ // ===========================================================================
664
+ // _data handlers
665
+ // ===========================================================================
666
+ handleListData(url) {
667
+ const type = url.searchParams.get('type');
668
+ const limit = url.searchParams.get('limit');
669
+ const offset = url.searchParams.get('offset');
670
+ let query = 'SELECT * FROM _data';
671
+ const params = [];
672
+ if (type) {
673
+ query += ' WHERE type = ?';
674
+ params.push(type);
675
+ }
676
+ query += ' ORDER BY rowid ASC';
677
+ if (limit) {
678
+ query += ' LIMIT ?';
679
+ params.push(parseInt(limit, 10));
680
+ }
681
+ if (offset) {
682
+ query += ' OFFSET ?';
683
+ params.push(parseInt(offset, 10));
684
+ }
685
+ const rows = this.sql.exec(query, ...params).toArray();
686
+ const results = rows.map((row) => this.deserializeDataRow(row));
687
+ return Response.json(results);
688
+ }
689
+ async handleInsertData(request) {
690
+ let body;
691
+ try {
692
+ body = (await request.json());
693
+ }
694
+ catch {
695
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
696
+ }
697
+ const { type, data } = body;
698
+ let { id } = body;
699
+ if (!type) {
700
+ return Response.json({ error: 'type field is required' }, { status: 400 });
701
+ }
702
+ if (!id) {
703
+ id = crypto.randomUUID();
704
+ }
705
+ // Check for duplicate ID
706
+ const existing = this.sql.exec('SELECT id FROM _data WHERE id = ?', id).toArray();
707
+ if (existing.length > 0) {
708
+ return Response.json({ error: 'Record with this id already exists' }, { status: 409 });
709
+ }
710
+ const now = new Date().toISOString();
711
+ const dataJson = JSON.stringify(data ?? {});
712
+ this.sql.exec('INSERT INTO _data (id, type, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', id, type, dataJson, now, now);
713
+ // Emit Type.created event
714
+ const actor = request.headers.get('X-Actor') ?? 'system';
715
+ this.emitEvent({
716
+ event: `${type}.created`,
717
+ actor,
718
+ object: `${type}/${id}`,
719
+ data: data ?? {},
720
+ });
721
+ // Note: Embedding generation happens lazily when first queried or via batch/warmup/search
722
+ // This allows fine-grained control over when embeddings are generated
723
+ const result = {
724
+ id,
725
+ type,
726
+ data: data ?? {},
727
+ created_at: now,
728
+ updated_at: now,
729
+ };
730
+ return Response.json(result);
731
+ }
732
+ handleGetData(id) {
733
+ const rows = this.sql.exec('SELECT * FROM _data WHERE id = ?', id).toArray();
734
+ if (rows.length === 0) {
735
+ return Response.json({ error: 'Not found' }, { status: 404 });
736
+ }
737
+ return Response.json(this.deserializeDataRow(rows[0]));
738
+ }
739
+ async handleUpdateData(id, request) {
740
+ const rows = this.sql.exec('SELECT * FROM _data WHERE id = ?', id).toArray();
741
+ if (rows.length === 0) {
742
+ return Response.json({ error: 'Not found' }, { status: 404 });
743
+ }
744
+ const existing = this.deserializeDataRow(rows[0]);
745
+ const body = (await request.json());
746
+ // Merge data fields (shallow merge)
747
+ const mergedData = { ...existing.data, ...(body.data ?? {}) };
748
+ const now = new Date().toISOString();
749
+ const dataJson = JSON.stringify(mergedData);
750
+ this.sql.exec('UPDATE _data SET data = ?, updated_at = ? WHERE id = ?', dataJson, now, id);
751
+ // Emit Type.updated event
752
+ const actor = request.headers.get('X-Actor') ?? 'system';
753
+ this.emitEvent({
754
+ event: `${existing.type}.updated`,
755
+ actor,
756
+ object: `${existing.type}/${id}`,
757
+ data: mergedData,
758
+ previousData: existing.data,
759
+ });
760
+ // If embedding exists, update it with new content
761
+ const existingEmbedding = this.sql
762
+ .exec('SELECT id FROM _embeddings WHERE entity_type = ? AND entity_id = ?', existing.type, id)
763
+ .toArray();
764
+ if (existingEmbedding.length > 0) {
765
+ await this.generateEmbeddingForEntity(existing.type, id, mergedData).catch(() => {
766
+ // Silently ignore embedding generation errors on update
767
+ });
768
+ }
769
+ const result = {
770
+ id: existing.id,
771
+ type: existing.type,
772
+ data: mergedData,
773
+ created_at: existing.created_at,
774
+ updated_at: now,
775
+ };
776
+ return Response.json(result);
777
+ }
778
+ handleDeleteData(id, url, request) {
779
+ const rows = this.sql.exec('SELECT * FROM _data WHERE id = ?', id).toArray();
780
+ if (rows.length === 0) {
781
+ return Response.json({ deleted: false });
782
+ }
783
+ const entity = this.deserializeDataRow(rows[0]);
784
+ // Check for cascade option
785
+ const cascade = url?.searchParams.get('cascade') === 'true';
786
+ const cascadeDepth = parseInt(url?.searchParams.get('cascadeDepth') ?? '999', 10);
787
+ let cascadeDeleted;
788
+ if (cascade) {
789
+ // Perform cascade delete with depth control
790
+ cascadeDeleted = this.cascadeDelete(id, cascadeDepth, new Set([id]));
791
+ }
792
+ // Delete the record
793
+ this.sql.exec('DELETE FROM _data WHERE id = ?', id);
794
+ // Cascade: remove relationships involving this id
795
+ this.sql.exec('DELETE FROM _rels WHERE from_id = ? OR to_id = ?', id, id);
796
+ // Delete embedding for this entity
797
+ this.sql.exec('DELETE FROM _embeddings WHERE entity_type = ? AND entity_id = ?', entity['type'], id);
798
+ // Emit Type.deleted event
799
+ const actor = request?.headers.get('X-Actor') ?? 'system';
800
+ this.emitEvent({
801
+ event: `${entity['type']}.deleted`,
802
+ actor,
803
+ object: `${entity['type']}/${id}`,
804
+ data: entity['data'],
805
+ });
806
+ const result = { deleted: true };
807
+ if (cascadeDeleted !== undefined) {
808
+ result['cascadeDeleted'] = cascadeDeleted;
809
+ }
810
+ return Response.json(result);
811
+ }
812
+ /**
813
+ * Cascade delete related entities up to a given depth
814
+ */
815
+ cascadeDelete(fromId, depth, visited) {
816
+ if (depth <= 0)
817
+ return [];
818
+ const deleted = [];
819
+ // Get all outgoing relationships from this entity
820
+ const rels = this.sql.exec('SELECT to_id FROM _rels WHERE from_id = ?', fromId).toArray();
821
+ for (const rel of rels) {
822
+ const relRow = rel;
823
+ const toId = relRow.to_id;
824
+ if (visited.has(toId))
825
+ continue;
826
+ visited.add(toId);
827
+ // Recursively delete related entities
828
+ const nested = this.cascadeDelete(toId, depth - 1, visited);
829
+ deleted.push(...nested);
830
+ // Delete the entity
831
+ const exists = this.sql.exec('SELECT id FROM _data WHERE id = ?', toId).toArray();
832
+ if (exists.length > 0) {
833
+ this.sql.exec('DELETE FROM _data WHERE id = ?', toId);
834
+ this.sql.exec('DELETE FROM _rels WHERE from_id = ? OR to_id = ?', toId, toId);
835
+ deleted.push(toId);
836
+ }
837
+ }
838
+ return deleted;
839
+ }
840
+ // ===========================================================================
841
+ // _rels handlers
842
+ // ===========================================================================
843
+ handleQueryRels(url) {
844
+ const from_id = url.searchParams.get('from_id');
845
+ const to_id = url.searchParams.get('to_id');
846
+ const relation = url.searchParams.get('relation');
847
+ let query = 'SELECT * FROM _rels WHERE 1=1';
848
+ const params = [];
849
+ if (from_id) {
850
+ query += ' AND from_id = ?';
851
+ params.push(from_id);
852
+ }
853
+ if (to_id) {
854
+ query += ' AND to_id = ?';
855
+ params.push(to_id);
856
+ }
857
+ if (relation) {
858
+ query += ' AND relation = ?';
859
+ params.push(relation);
860
+ }
861
+ const rows = this.sql.exec(query, ...params).toArray();
862
+ const results = rows.map((row) => this.deserializeRelRow(row));
863
+ return Response.json(results);
864
+ }
865
+ async handleCreateRel(request) {
866
+ const body = (await request.json());
867
+ const { from_id, relation, to_id, metadata } = body;
868
+ // Validate required fields
869
+ if (!from_id || !to_id) {
870
+ return Response.json({ error: 'from_id, relation, and to_id are required' }, { status: 400 });
871
+ }
872
+ // Validate relation is not empty
873
+ if (!relation || relation.trim() === '') {
874
+ return Response.json({ error: 'relation cannot be empty' }, { status: 400 });
875
+ }
876
+ // Validate that both entities exist
877
+ const fromExists = this.sql.exec('SELECT id FROM _data WHERE id = ?', from_id).toArray();
878
+ const toExists = this.sql.exec('SELECT id FROM _data WHERE id = ?', to_id).toArray();
879
+ if (fromExists.length === 0) {
880
+ return Response.json({ error: `Source entity '${from_id}' does not exist` }, { status: 400 });
881
+ }
882
+ if (toExists.length === 0) {
883
+ return Response.json({ error: `Target entity '${to_id}' does not exist` }, { status: 400 });
884
+ }
885
+ const metadataJson = metadata ? JSON.stringify(metadata) : null;
886
+ const now = new Date().toISOString();
887
+ const actor = request.headers.get('X-Actor') ?? 'system';
888
+ try {
889
+ this.sql.exec('INSERT INTO _rels (from_id, relation, to_id, metadata, created_at) VALUES (?, ?, ?, ?, ?)', from_id, relation, to_id, metadataJson, now);
890
+ // Emit relationship.created event
891
+ this.emitEvent({
892
+ event: 'relationship.created',
893
+ actor,
894
+ object: `${from_id}->${relation}->${to_id}`,
895
+ data: { from_id, relation, to_id, metadata: metadata ?? null },
896
+ });
897
+ }
898
+ catch (err) {
899
+ // Handle duplicate composite key -- upsert
900
+ const errMsg = err instanceof Error ? err.message : '';
901
+ if (errMsg.includes('UNIQUE constraint')) {
902
+ this.sql.exec('UPDATE _rels SET metadata = ? WHERE from_id = ? AND relation = ? AND to_id = ?', metadataJson, from_id, relation, to_id);
903
+ // Re-fetch to get the original created_at
904
+ const rows = this.sql
905
+ .exec('SELECT created_at FROM _rels WHERE from_id = ? AND relation = ? AND to_id = ?', from_id, relation, to_id)
906
+ .toArray();
907
+ const relRow = rows[0];
908
+ const result = {
909
+ from_id,
910
+ relation,
911
+ to_id,
912
+ metadata: metadata ?? null,
913
+ created_at: relRow?.created_at ?? now,
914
+ };
915
+ return Response.json(result);
916
+ }
917
+ else {
918
+ throw err;
919
+ }
920
+ }
921
+ const result = {
922
+ from_id,
923
+ relation,
924
+ to_id,
925
+ metadata: metadata ?? null,
926
+ created_at: now,
927
+ };
928
+ return Response.json(result);
929
+ }
930
+ async handleDeleteRel(request) {
931
+ const body = (await request.json());
932
+ const { from_id, relation, to_id } = body;
933
+ const existing = this.sql
934
+ .exec('SELECT * FROM _rels WHERE from_id = ? AND relation = ? AND to_id = ?', from_id, relation, to_id)
935
+ .toArray();
936
+ if (existing.length === 0) {
937
+ return Response.json({ deleted: false });
938
+ }
939
+ const existingRel = this.deserializeRelRow(existing[0]);
940
+ this.sql.exec('DELETE FROM _rels WHERE from_id = ? AND relation = ? AND to_id = ?', from_id, relation, to_id);
941
+ // Emit relationship.deleted event
942
+ const actor = request.headers.get('X-Actor') ?? 'system';
943
+ this.emitEvent({
944
+ event: 'relationship.deleted',
945
+ actor,
946
+ object: `${from_id}->${relation}->${to_id}`,
947
+ data: { from_id, relation, to_id, metadata: existingRel['metadata'] },
948
+ });
949
+ return Response.json({ deleted: true });
950
+ }
951
+ // ===========================================================================
952
+ // Traverse handler
953
+ // ===========================================================================
954
+ handleTraverse(url) {
955
+ const from_id = url.searchParams.get('from_id');
956
+ const to_id = url.searchParams.get('to_id');
957
+ const id = url.searchParams.get('id'); // for bidirectional traversal
958
+ const relationParam = url.searchParams.get('relation');
959
+ const typeFilter = url.searchParams.get('type');
960
+ const direction = url.searchParams.get('direction'); // 'in', 'out', 'both'
961
+ const maxDepthParam = url.searchParams.get('maxDepth');
962
+ const includeMetadata = url.searchParams.get('includeMetadata') === 'true';
963
+ // Support multi-hop traversal via comma-separated relations
964
+ const relations = relationParam ? relationParam.split(',') : [];
965
+ const maxDepth = maxDepthParam ? parseInt(maxDepthParam, 10) : relations.length;
966
+ // Bidirectional traversal: id + relation + direction=both
967
+ if (id && relations.length > 0 && direction === 'both') {
968
+ return this.handleBidirectionalTraverse(id, relations[0], typeFilter, includeMetadata);
969
+ }
970
+ // Forward traversal with from_id and direction=out or no direction
971
+ if (from_id && relations.length > 0 && direction !== 'in') {
972
+ return this.handleForwardTraverse(from_id, relations, typeFilter, maxDepth, includeMetadata);
973
+ }
974
+ // Reverse traversal: to_id + relation (with direction=in)
975
+ if (to_id && relations.length > 0) {
976
+ return this.handleReverseTraverse(to_id, relations[0], typeFilter, includeMetadata);
977
+ }
978
+ // Forward traversal with from_id (no relation means all outgoing)
979
+ if (from_id && !relationParam) {
980
+ const rows = this.sql.exec('SELECT to_id FROM _rels WHERE from_id = ?', from_id).toArray();
981
+ const toIds = [...new Set(rows.map((r) => r.to_id))];
982
+ if (toIds.length === 0) {
983
+ return Response.json([]);
984
+ }
985
+ return this.fetchEntitiesByIds(toIds, typeFilter, includeMetadata ? from_id : undefined);
986
+ }
987
+ return Response.json([]);
988
+ }
989
+ /**
990
+ * Handle bidirectional traversal (both incoming and outgoing)
991
+ */
992
+ handleBidirectionalTraverse(entityId, relation, typeFilter, includeMetadata) {
993
+ // Get outgoing relationships (entity -> X)
994
+ const outgoing = this.sql
995
+ .exec('SELECT to_id, metadata FROM _rels WHERE from_id = ? AND relation = ?', entityId, relation)
996
+ .toArray();
997
+ // Get incoming relationships (X -> entity)
998
+ const incoming = this.sql
999
+ .exec('SELECT from_id, metadata FROM _rels WHERE to_id = ? AND relation = ?', entityId, relation)
1000
+ .toArray();
1001
+ const resultIds = new Set();
1002
+ const metadataMap = new Map();
1003
+ for (const row of outgoing) {
1004
+ const relRow = row;
1005
+ const toId = relRow.to_id;
1006
+ resultIds.add(toId);
1007
+ if (includeMetadata && relRow.metadata) {
1008
+ const meta = typeof relRow.metadata === 'string' ? JSON.parse(relRow.metadata) : relRow.metadata;
1009
+ metadataMap.set(toId, meta);
1010
+ }
1011
+ }
1012
+ for (const row of incoming) {
1013
+ const relRow = row;
1014
+ const fromId = relRow.from_id;
1015
+ resultIds.add(fromId);
1016
+ if (includeMetadata && relRow.metadata) {
1017
+ const meta = typeof relRow.metadata === 'string' ? JSON.parse(relRow.metadata) : relRow.metadata;
1018
+ metadataMap.set(fromId, meta);
1019
+ }
1020
+ }
1021
+ if (resultIds.size === 0) {
1022
+ return Response.json([]);
1023
+ }
1024
+ return this.fetchEntitiesByIds([...resultIds], typeFilter, includeMetadata ? entityId : undefined, includeMetadata ? metadataMap : undefined);
1025
+ }
1026
+ /**
1027
+ * Handle forward (outgoing) traversal with depth control
1028
+ */
1029
+ handleForwardTraverse(fromId, relations, typeFilter, maxDepth, includeMetadata) {
1030
+ let currentIds = [fromId];
1031
+ const metadataMap = new Map();
1032
+ // For single-hop traversal, don't add source to visited to allow self-reference
1033
+ const visited = new Set();
1034
+ if (relations.length > 1) {
1035
+ visited.add(fromId);
1036
+ }
1037
+ // Limit traversal depth
1038
+ const effectiveRelations = relations.slice(0, maxDepth);
1039
+ for (let i = 0; i < effectiveRelations.length; i++) {
1040
+ const rel = effectiveRelations[i];
1041
+ if (currentIds.length === 0)
1042
+ break;
1043
+ const placeholders = currentIds.map(() => '?').join(',');
1044
+ const rows = this.sql
1045
+ .exec(`SELECT to_id, metadata FROM _rels WHERE from_id IN (${placeholders}) AND relation = ?`, ...currentIds, rel)
1046
+ .toArray();
1047
+ const nextIds = new Set();
1048
+ for (const row of rows) {
1049
+ const relRow = row;
1050
+ const toId = relRow.to_id;
1051
+ // For the last hop, don't prevent returning visited nodes (allows self-reference results)
1052
+ // For intermediate hops, use cycle detection to prevent infinite loops
1053
+ const isLastHop = i === effectiveRelations.length - 1;
1054
+ if (isLastHop || !visited.has(toId)) {
1055
+ if (!isLastHop) {
1056
+ visited.add(toId);
1057
+ }
1058
+ nextIds.add(toId);
1059
+ if (includeMetadata && relRow.metadata) {
1060
+ const meta = typeof relRow.metadata === 'string' ? JSON.parse(relRow.metadata) : relRow.metadata;
1061
+ metadataMap.set(toId, meta);
1062
+ }
1063
+ }
1064
+ }
1065
+ currentIds = [...nextIds];
1066
+ }
1067
+ if (currentIds.length === 0) {
1068
+ return Response.json([]);
1069
+ }
1070
+ return this.fetchEntitiesByIds(currentIds, typeFilter, includeMetadata ? fromId : undefined, includeMetadata ? metadataMap : undefined);
1071
+ }
1072
+ /**
1073
+ * Handle reverse (incoming) traversal
1074
+ */
1075
+ handleReverseTraverse(toId, relation, typeFilter, includeMetadata) {
1076
+ const rows = this.sql
1077
+ .exec('SELECT from_id, metadata FROM _rels WHERE to_id = ? AND relation = ?', toId, relation)
1078
+ .toArray();
1079
+ const fromIds = [];
1080
+ const metadataMap = new Map();
1081
+ for (const row of rows) {
1082
+ const relRow = row;
1083
+ const fromId = relRow.from_id;
1084
+ fromIds.push(fromId);
1085
+ if (includeMetadata && relRow.metadata) {
1086
+ const meta = typeof relRow.metadata === 'string' ? JSON.parse(relRow.metadata) : relRow.metadata;
1087
+ metadataMap.set(fromId, meta);
1088
+ }
1089
+ }
1090
+ if (fromIds.length === 0) {
1091
+ return Response.json([]);
1092
+ }
1093
+ return this.fetchEntitiesByIds([...new Set(fromIds)], typeFilter, includeMetadata ? toId : undefined, includeMetadata ? metadataMap : undefined);
1094
+ }
1095
+ /**
1096
+ * Fetch entities by IDs with optional type filter and metadata
1097
+ */
1098
+ fetchEntitiesByIds(ids, typeFilter, _sourceId, metadataMap) {
1099
+ const placeholders = ids.map(() => '?').join(',');
1100
+ let query = `SELECT * FROM _data WHERE id IN (${placeholders})`;
1101
+ const params = [...ids];
1102
+ if (typeFilter) {
1103
+ query += ' AND type = ?';
1104
+ params.push(typeFilter);
1105
+ }
1106
+ const records = this.sql.exec(query, ...params).toArray();
1107
+ const results = records.map((r) => {
1108
+ const entity = this.deserializeDataRow(r);
1109
+ if (metadataMap && metadataMap.has(entity.id)) {
1110
+ const relData = metadataMap.get(entity.id);
1111
+ if (relData !== undefined) {
1112
+ entity.$rel = relData;
1113
+ }
1114
+ }
1115
+ return entity;
1116
+ });
1117
+ return Response.json(results);
1118
+ }
1119
+ /**
1120
+ * Handle POST /traverse/filter - filter traversal by metadata
1121
+ */
1122
+ async handleTraverseFilter(request) {
1123
+ let body;
1124
+ try {
1125
+ body = (await request.json());
1126
+ }
1127
+ catch {
1128
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1129
+ }
1130
+ const { from_id, relation, filter: metadataFilter } = body;
1131
+ if (!from_id || !relation) {
1132
+ return Response.json({ error: 'from_id and relation are required' }, { status: 400 });
1133
+ }
1134
+ // Get all relationships matching from_id and relation
1135
+ const rows = this.sql
1136
+ .exec('SELECT to_id, metadata FROM _rels WHERE from_id = ? AND relation = ?', from_id, relation)
1137
+ .toArray();
1138
+ const matchingIds = [];
1139
+ for (const row of rows) {
1140
+ const relRow = row;
1141
+ const toId = relRow.to_id;
1142
+ const rawMetadata = relRow.metadata;
1143
+ if (!metadataFilter) {
1144
+ matchingIds.push(toId);
1145
+ continue;
1146
+ }
1147
+ // Parse metadata
1148
+ const metadata = rawMetadata
1149
+ ? typeof rawMetadata === 'string'
1150
+ ? JSON.parse(rawMetadata)
1151
+ : rawMetadata
1152
+ : {};
1153
+ // Apply metadata filter
1154
+ if (this.matchesMetadataFilter(metadata, metadataFilter)) {
1155
+ matchingIds.push(toId);
1156
+ }
1157
+ }
1158
+ if (matchingIds.length === 0) {
1159
+ return Response.json([]);
1160
+ }
1161
+ // Fetch the entities
1162
+ const placeholders = matchingIds.map(() => '?').join(',');
1163
+ const entities = this.sql
1164
+ .exec(`SELECT * FROM _data WHERE id IN (${placeholders})`, ...matchingIds)
1165
+ .toArray();
1166
+ return Response.json(entities.map((r) => this.deserializeDataRow(r)));
1167
+ }
1168
+ /**
1169
+ * Check if metadata matches a filter with operator support
1170
+ */
1171
+ matchesMetadataFilter(metadata, filter) {
1172
+ for (const [field, value] of Object.entries(filter)) {
1173
+ const actual = metadata[field];
1174
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1175
+ // Handle operators
1176
+ const ops = value;
1177
+ for (const [op, opValue] of Object.entries(ops)) {
1178
+ switch (op) {
1179
+ case '$gt':
1180
+ if (!(typeof actual === 'number' && actual > opValue))
1181
+ return false;
1182
+ break;
1183
+ case '$gte':
1184
+ if (!(typeof actual === 'number' && actual >= opValue))
1185
+ return false;
1186
+ break;
1187
+ case '$lt':
1188
+ if (!(typeof actual === 'number' && actual < opValue))
1189
+ return false;
1190
+ break;
1191
+ case '$lte':
1192
+ if (!(typeof actual === 'number' && actual <= opValue))
1193
+ return false;
1194
+ break;
1195
+ case '$ne':
1196
+ if (actual === opValue)
1197
+ return false;
1198
+ break;
1199
+ case '$in':
1200
+ if (!Array.isArray(opValue) || !opValue.includes(actual))
1201
+ return false;
1202
+ break;
1203
+ }
1204
+ }
1205
+ }
1206
+ else {
1207
+ // Simple equality
1208
+ if (actual !== value)
1209
+ return false;
1210
+ }
1211
+ }
1212
+ return true;
1213
+ }
1214
+ /**
1215
+ * Handle PATCH /rels - update relationship metadata
1216
+ */
1217
+ async handleUpdateRel(request) {
1218
+ let body;
1219
+ try {
1220
+ body = (await request.json());
1221
+ }
1222
+ catch {
1223
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1224
+ }
1225
+ const { from_id, relation, to_id, metadata } = body;
1226
+ if (!from_id || !relation || !to_id) {
1227
+ return Response.json({ error: 'from_id, relation, and to_id are required' }, { status: 400 });
1228
+ }
1229
+ // Check if relationship exists
1230
+ const existing = this.sql
1231
+ .exec('SELECT * FROM _rels WHERE from_id = ? AND relation = ? AND to_id = ?', from_id, relation, to_id)
1232
+ .toArray();
1233
+ if (existing.length === 0) {
1234
+ return Response.json({ error: 'Relationship not found' }, { status: 404 });
1235
+ }
1236
+ const existingRel = this.deserializeRelRow(existing[0]);
1237
+ // Update metadata
1238
+ const metadataJson = metadata ? JSON.stringify(metadata) : null;
1239
+ this.sql.exec('UPDATE _rels SET metadata = ? WHERE from_id = ? AND relation = ? AND to_id = ?', metadataJson, from_id, relation, to_id);
1240
+ // Emit relationship.updated event
1241
+ const actor = request.headers.get('X-Actor') ?? 'system';
1242
+ this.emitEvent({
1243
+ event: 'relationship.updated',
1244
+ actor,
1245
+ object: `${from_id}->${relation}->${to_id}`,
1246
+ data: { from_id, relation, to_id, metadata: metadata ?? null },
1247
+ previousData: { from_id, relation, to_id, metadata: existingRel['metadata'] },
1248
+ });
1249
+ return Response.json({ from_id, relation, to_id, metadata: metadata ?? null });
1250
+ }
1251
+ // ===========================================================================
1252
+ // Meta handlers
1253
+ // ===========================================================================
1254
+ handleGetIndexes() {
1255
+ const rows = this.sql
1256
+ .exec(`
1257
+ SELECT name, tbl_name as table_name, sql
1258
+ FROM sqlite_master
1259
+ WHERE type = 'index' AND sql IS NOT NULL
1260
+ `)
1261
+ .toArray();
1262
+ return Response.json(rows);
1263
+ }
1264
+ handleGetVersion() {
1265
+ const rows = this.sql.exec(`SELECT value FROM _meta WHERE key = 'version'`).toArray();
1266
+ if (rows.length === 0) {
1267
+ return Response.json({ version: 1 });
1268
+ }
1269
+ const metaRow = rows[0];
1270
+ return Response.json({ version: parseInt(metaRow.value, 10) });
1271
+ }
1272
+ // ===========================================================================
1273
+ // Query handlers
1274
+ // ===========================================================================
1275
+ /**
1276
+ * Validate a field name to prevent SQL injection.
1277
+ * Only allows alphanumeric characters, underscores, and dots for nested fields.
1278
+ */
1279
+ isValidFieldName(fieldName) {
1280
+ return /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(fieldName);
1281
+ }
1282
+ /**
1283
+ * Build SQL WHERE clause from a where object.
1284
+ * Returns [clause, params] tuple where clause includes the WHERE keyword.
1285
+ */
1286
+ /**
1287
+ * Convert a value for SQLite JSON comparison.
1288
+ * Booleans need to be converted to 1/0 because SQLite stores JSON booleans as integers.
1289
+ */
1290
+ toSqliteValue(value) {
1291
+ if (typeof value === 'boolean') {
1292
+ return value ? 1 : 0;
1293
+ }
1294
+ return value;
1295
+ }
1296
+ buildWhereClause(type, where) {
1297
+ const conditions = ['type = ?'];
1298
+ const params = [type];
1299
+ if (where) {
1300
+ for (const [field, value] of Object.entries(where)) {
1301
+ // Validate field name to prevent injection
1302
+ if (!this.isValidFieldName(field)) {
1303
+ continue; // Skip invalid field names silently
1304
+ }
1305
+ // Handle nested field paths like "profile.location"
1306
+ const jsonPath = field.includes('.') ? `$.${field}` : `$.${field}`;
1307
+ if (value === null) {
1308
+ conditions.push(`json_extract(data, ?) IS NULL`);
1309
+ params.push(jsonPath);
1310
+ }
1311
+ else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1312
+ // Handle operators: $gt, $lt, $gte, $lte, $in, $ne
1313
+ const ops = value;
1314
+ for (const [op, opValue] of Object.entries(ops)) {
1315
+ const sqliteValue = this.toSqliteValue(opValue);
1316
+ switch (op) {
1317
+ case '$gt':
1318
+ conditions.push(`json_extract(data, ?) > ?`);
1319
+ params.push(jsonPath, sqliteValue);
1320
+ break;
1321
+ case '$lt':
1322
+ conditions.push(`json_extract(data, ?) < ?`);
1323
+ params.push(jsonPath, sqliteValue);
1324
+ break;
1325
+ case '$gte':
1326
+ conditions.push(`json_extract(data, ?) >= ?`);
1327
+ params.push(jsonPath, sqliteValue);
1328
+ break;
1329
+ case '$lte':
1330
+ conditions.push(`json_extract(data, ?) <= ?`);
1331
+ params.push(jsonPath, sqliteValue);
1332
+ break;
1333
+ case '$ne':
1334
+ conditions.push(`json_extract(data, ?) != ?`);
1335
+ params.push(jsonPath, sqliteValue);
1336
+ break;
1337
+ case '$in':
1338
+ if (Array.isArray(opValue) && opValue.length > 0) {
1339
+ const placeholders = opValue.map(() => '?').join(',');
1340
+ conditions.push(`json_extract(data, ?) IN (${placeholders})`);
1341
+ params.push(jsonPath, ...opValue.map((v) => this.toSqliteValue(v)));
1342
+ }
1343
+ break;
1344
+ }
1345
+ }
1346
+ }
1347
+ else {
1348
+ // Simple equality - convert booleans to 1/0 for SQLite JSON comparison
1349
+ conditions.push(`json_extract(data, ?) = ?`);
1350
+ params.push(jsonPath, this.toSqliteValue(value));
1351
+ }
1352
+ }
1353
+ }
1354
+ return {
1355
+ clause: conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '',
1356
+ params,
1357
+ };
1358
+ }
1359
+ /**
1360
+ * Build ORDER BY clause from orderBy and order parameters.
1361
+ */
1362
+ buildOrderByClause(orderBy, order) {
1363
+ if (!orderBy || !this.isValidFieldName(orderBy)) {
1364
+ return ' ORDER BY rowid ASC';
1365
+ }
1366
+ const direction = order?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
1367
+ const jsonPath = `$.${orderBy}`;
1368
+ return ` ORDER BY json_extract(data, '${jsonPath}') ${direction}`;
1369
+ }
1370
+ /**
1371
+ * Handle GET /query/list - simple query via URL params
1372
+ */
1373
+ handleQueryList(url) {
1374
+ const type = url.searchParams.get('type');
1375
+ if (!type) {
1376
+ return Response.json({ error: 'type parameter is required' }, { status: 400 });
1377
+ }
1378
+ const limit = url.searchParams.get('limit');
1379
+ const offset = url.searchParams.get('offset');
1380
+ const orderBy = url.searchParams.get('orderBy');
1381
+ const order = url.searchParams.get('order');
1382
+ const { clause, params } = this.buildWhereClause(type);
1383
+ let query = `SELECT * FROM _data${clause}`;
1384
+ query += this.buildOrderByClause(orderBy, order);
1385
+ const limitNum = limit ? parseInt(limit, 10) : null;
1386
+ const offsetNum = offset ? parseInt(offset, 10) : null;
1387
+ // OFFSET requires LIMIT in SQLite, so add a very large default LIMIT if offset is specified without limit
1388
+ if (offsetNum !== null && offsetNum >= 0) {
1389
+ if (limitNum !== null && limitNum >= 0) {
1390
+ query += ' LIMIT ?';
1391
+ params.push(limitNum);
1392
+ }
1393
+ else {
1394
+ // Use a very large limit when only offset is specified
1395
+ query += ' LIMIT -1';
1396
+ }
1397
+ query += ' OFFSET ?';
1398
+ params.push(offsetNum);
1399
+ }
1400
+ else if (limitNum !== null && limitNum >= 0) {
1401
+ query += ' LIMIT ?';
1402
+ params.push(limitNum);
1403
+ }
1404
+ const rows = this.sql.exec(query, ...params).toArray();
1405
+ const results = rows.map((row) => this.deserializeDataRow(row));
1406
+ return Response.json(results);
1407
+ }
1408
+ /**
1409
+ * Handle POST /query/list - complex query via JSON body
1410
+ */
1411
+ async handleQueryListPost(request) {
1412
+ let body;
1413
+ try {
1414
+ body = (await request.json());
1415
+ }
1416
+ catch {
1417
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1418
+ }
1419
+ const { type, where, orderBy, order, limit, offset } = body;
1420
+ if (!type) {
1421
+ return Response.json({ error: 'type field is required' }, { status: 400 });
1422
+ }
1423
+ const { clause, params } = this.buildWhereClause(type, where);
1424
+ let query = `SELECT * FROM _data${clause}`;
1425
+ query += this.buildOrderByClause(orderBy, order);
1426
+ // OFFSET requires LIMIT in SQLite, so add a very large default LIMIT if offset is specified without limit
1427
+ if (typeof offset === 'number' && offset >= 0) {
1428
+ if (typeof limit === 'number' && limit >= 0) {
1429
+ query += ' LIMIT ?';
1430
+ params.push(limit);
1431
+ }
1432
+ else {
1433
+ // Use a very large limit when only offset is specified
1434
+ query += ' LIMIT -1';
1435
+ }
1436
+ query += ' OFFSET ?';
1437
+ params.push(offset);
1438
+ }
1439
+ else if (typeof limit === 'number' && limit >= 0) {
1440
+ query += ' LIMIT ?';
1441
+ params.push(limit);
1442
+ }
1443
+ const rows = this.sql.exec(query, ...params).toArray();
1444
+ const results = rows.map((row) => this.deserializeDataRow(row));
1445
+ return Response.json(results);
1446
+ }
1447
+ /**
1448
+ * Handle POST /query/find - find first matching record
1449
+ */
1450
+ async handleQueryFind(request) {
1451
+ let body;
1452
+ try {
1453
+ body = (await request.json());
1454
+ }
1455
+ catch {
1456
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1457
+ }
1458
+ const { type, where, orderBy, order } = body;
1459
+ if (!type) {
1460
+ return Response.json({ error: 'type field is required' }, { status: 400 });
1461
+ }
1462
+ const { clause, params } = this.buildWhereClause(type, where);
1463
+ let query = `SELECT * FROM _data${clause}`;
1464
+ query += this.buildOrderByClause(orderBy, order);
1465
+ query += ' LIMIT 1';
1466
+ const rows = this.sql.exec(query, ...params).toArray();
1467
+ if (rows.length === 0) {
1468
+ return Response.json(null);
1469
+ }
1470
+ return Response.json(this.deserializeDataRow(rows[0]));
1471
+ }
1472
+ /**
1473
+ * Escape special characters in LIKE patterns
1474
+ */
1475
+ escapeLikePattern(pattern) {
1476
+ // Escape %, _, and \ for LIKE
1477
+ return pattern.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
1478
+ }
1479
+ /**
1480
+ * Calculate a simple relevance score based on term matches
1481
+ */
1482
+ calculateRelevanceScore(text, query) {
1483
+ const lowerText = text.toLowerCase();
1484
+ const lowerQuery = query.toLowerCase();
1485
+ const terms = lowerQuery.split(/\s+/).filter((t) => t.length > 0);
1486
+ if (terms.length === 0)
1487
+ return 0;
1488
+ let matchCount = 0;
1489
+ let exactMatch = lowerText.includes(lowerQuery);
1490
+ for (const term of terms) {
1491
+ if (lowerText.includes(term)) {
1492
+ matchCount++;
1493
+ }
1494
+ }
1495
+ // Score: exact match gets bonus, then percentage of terms matched
1496
+ const baseScore = matchCount / terms.length;
1497
+ return exactMatch ? Math.min(1, baseScore + 0.3) : baseScore;
1498
+ }
1499
+ /**
1500
+ * Handle POST /query/search - full-text search
1501
+ */
1502
+ async handleQuerySearch(request) {
1503
+ let body;
1504
+ try {
1505
+ body = (await request.json());
1506
+ }
1507
+ catch {
1508
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1509
+ }
1510
+ const { type, query: searchQuery, fields, limit, minScore } = body;
1511
+ if (!type) {
1512
+ return Response.json({ error: 'type field is required' }, { status: 400 });
1513
+ }
1514
+ if (!searchQuery || typeof searchQuery !== 'string') {
1515
+ return Response.json({ error: 'query field is required' }, { status: 400 });
1516
+ }
1517
+ // Escape the search query for LIKE
1518
+ const escapedQuery = this.escapeLikePattern(searchQuery);
1519
+ const likePattern = `%${escapedQuery}%`;
1520
+ // Get all records of this type
1521
+ const rows = this.sql.exec('SELECT * FROM _data WHERE type = ?', type).toArray();
1522
+ // Filter and score results
1523
+ const results = [];
1524
+ for (const row of rows) {
1525
+ const record = this.deserializeDataRow(row);
1526
+ const data = record.data;
1527
+ // Determine which fields to search
1528
+ const searchFields = Array.isArray(fields) && fields.length > 0
1529
+ ? fields
1530
+ : Object.keys(data).filter((k) => typeof data[k] === 'string' || typeof data[k] === 'number');
1531
+ // Search across fields
1532
+ let maxScore = 0;
1533
+ let hasMatch = false;
1534
+ for (const field of searchFields) {
1535
+ const value = data[field];
1536
+ if (value === undefined || value === null)
1537
+ continue;
1538
+ const stringValue = String(value);
1539
+ // Case-insensitive comparison
1540
+ if (stringValue.toLowerCase().includes(searchQuery.toLowerCase())) {
1541
+ hasMatch = true;
1542
+ const fieldScore = this.calculateRelevanceScore(stringValue, searchQuery);
1543
+ maxScore = Math.max(maxScore, fieldScore);
1544
+ }
1545
+ }
1546
+ if (hasMatch) {
1547
+ // Apply minScore filter
1548
+ if (typeof minScore === 'number' && maxScore < minScore) {
1549
+ continue;
1550
+ }
1551
+ results.push({ row: record, score: maxScore });
1552
+ }
1553
+ }
1554
+ // Sort by score descending
1555
+ results.sort((a, b) => b.score - a.score);
1556
+ // Apply limit
1557
+ const limitNum = typeof limit === 'number' && limit > 0 ? limit : results.length;
1558
+ const limited = results.slice(0, limitNum);
1559
+ // Add score to results
1560
+ const finalResults = limited.map(({ row, score }) => ({
1561
+ ...row,
1562
+ $score: score,
1563
+ }));
1564
+ return Response.json(finalResults);
1565
+ }
1566
+ // ===========================================================================
1567
+ // Events handlers
1568
+ // ===========================================================================
1569
+ /**
1570
+ * Emit an event to the _events table and pipeline
1571
+ */
1572
+ emitEvent(options) {
1573
+ const id = crypto.randomUUID();
1574
+ const timestamp = new Date().toISOString();
1575
+ const actor = options.actor ?? 'system';
1576
+ this.sql.exec(`INSERT INTO _events (id, event, actor, object, data, result, previous_data, timestamp)
1577
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, options.event, actor, options.object ?? null, options.data ? JSON.stringify(options.data) : null, options.result ?? null, options.previousData ? JSON.stringify(options.previousData) : null, timestamp);
1578
+ const eventRecord = {
1579
+ id,
1580
+ event: options.event,
1581
+ actor,
1582
+ object: options.object ?? null,
1583
+ data: options.data ?? null,
1584
+ result: options.result ?? null,
1585
+ previousData: options.previousData ?? null,
1586
+ timestamp,
1587
+ };
1588
+ // Add to pipeline buffer
1589
+ this.pipelineBuffer.push(eventRecord);
1590
+ this.pipelineStats.eventsProcessed++;
1591
+ // Auto-flush if buffer reaches batch size
1592
+ if (this.pipelineBuffer.length >= this.pipelineConfig.batchSize) {
1593
+ this.flushPipeline();
1594
+ }
1595
+ return eventRecord;
1596
+ }
1597
+ /**
1598
+ * Handle GET /events - list events with filtering
1599
+ */
1600
+ handleListEvents(url) {
1601
+ const event = url.searchParams.get('event');
1602
+ const object = url.searchParams.get('object');
1603
+ const since = url.searchParams.get('since');
1604
+ const until = url.searchParams.get('until');
1605
+ const limit = url.searchParams.get('limit');
1606
+ const offset = url.searchParams.get('offset');
1607
+ const cursor = url.searchParams.get('cursor');
1608
+ const order = url.searchParams.get('order') ?? 'desc';
1609
+ let query = 'SELECT * FROM _events WHERE 1=1';
1610
+ const params = [];
1611
+ // Filter by event type (with wildcard support)
1612
+ if (event) {
1613
+ if (event.startsWith('*.')) {
1614
+ // Wildcard at start: *.created matches Post.created, User.created, etc.
1615
+ const suffix = event.slice(2); // Remove '*.'
1616
+ query += ' AND event LIKE ?';
1617
+ params.push(`%.${suffix}`);
1618
+ }
1619
+ else if (event.endsWith('.*')) {
1620
+ // Wildcard at end: Post.* matches Post.created, Post.updated, etc.
1621
+ const prefix = event.slice(0, -2); // Remove '.*'
1622
+ query += ' AND event LIKE ?';
1623
+ params.push(`${prefix}.%`);
1624
+ }
1625
+ else {
1626
+ query += ' AND event = ?';
1627
+ params.push(event);
1628
+ }
1629
+ }
1630
+ // Filter by object
1631
+ if (object) {
1632
+ query += ' AND object = ?';
1633
+ params.push(object);
1634
+ }
1635
+ // Filter by time range
1636
+ if (since) {
1637
+ query += ' AND timestamp >= ?';
1638
+ params.push(since);
1639
+ }
1640
+ if (until) {
1641
+ query += ' AND timestamp <= ?';
1642
+ params.push(until);
1643
+ }
1644
+ // Cursor-based pagination (events after the cursor ID)
1645
+ if (cursor) {
1646
+ // Get the timestamp of the cursor event
1647
+ const cursorRows = this.sql
1648
+ .exec('SELECT timestamp FROM _events WHERE id = ?', cursor)
1649
+ .toArray();
1650
+ if (cursorRows.length > 0) {
1651
+ const cursorEvent = cursorRows[0];
1652
+ const cursorTs = cursorEvent.timestamp;
1653
+ if (order === 'asc') {
1654
+ query += ' AND (timestamp > ? OR (timestamp = ? AND id > ?))';
1655
+ params.push(cursorTs, cursorTs, cursor);
1656
+ }
1657
+ else {
1658
+ query += ' AND (timestamp < ? OR (timestamp = ? AND id < ?))';
1659
+ params.push(cursorTs, cursorTs, cursor);
1660
+ }
1661
+ }
1662
+ }
1663
+ // Order by timestamp
1664
+ query +=
1665
+ order === 'asc' ? ' ORDER BY timestamp ASC, id ASC' : ' ORDER BY timestamp DESC, id DESC';
1666
+ // Apply limit
1667
+ if (limit) {
1668
+ query += ' LIMIT ?';
1669
+ params.push(parseInt(limit, 10));
1670
+ }
1671
+ // Apply offset
1672
+ if (offset && !cursor) {
1673
+ query += ' OFFSET ?';
1674
+ params.push(parseInt(offset, 10));
1675
+ }
1676
+ const rows = this.sql.exec(query, ...params).toArray();
1677
+ const results = rows.map((row) => this.deserializeEventRow(row));
1678
+ return Response.json(results);
1679
+ }
1680
+ /**
1681
+ * Handle POST /events - create custom event
1682
+ */
1683
+ async handleCreateEvent(request) {
1684
+ let body;
1685
+ try {
1686
+ body = (await request.json());
1687
+ }
1688
+ catch {
1689
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1690
+ }
1691
+ const { event, actor, object, data, result } = body;
1692
+ if (!event || typeof event !== 'string') {
1693
+ return Response.json({ error: 'event field is required' }, { status: 400 });
1694
+ }
1695
+ const eventRecord = this.emitEvent({
1696
+ event: event,
1697
+ actor: actor ?? 'system',
1698
+ ...(object !== undefined && { object: object }),
1699
+ data,
1700
+ ...(result !== undefined && { result: result }),
1701
+ });
1702
+ return Response.json(eventRecord);
1703
+ }
1704
+ /**
1705
+ * Handle POST /events/replay - replay events
1706
+ */
1707
+ async handleReplayEvents(request) {
1708
+ let body;
1709
+ try {
1710
+ body = (await request.json());
1711
+ }
1712
+ catch {
1713
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1714
+ }
1715
+ const { source, object, event: eventFilter, since } = body;
1716
+ let query = 'SELECT * FROM _events WHERE 1=1';
1717
+ const params = [];
1718
+ if (object) {
1719
+ query += ' AND object = ?';
1720
+ params.push(object);
1721
+ }
1722
+ if (eventFilter) {
1723
+ query += ' AND event = ?';
1724
+ params.push(eventFilter);
1725
+ }
1726
+ if (since) {
1727
+ query += ' AND timestamp > ?';
1728
+ params.push(since);
1729
+ }
1730
+ query += ' ORDER BY timestamp ASC';
1731
+ const rows = this.sql.exec(query, ...params).toArray();
1732
+ const events = rows.map((row) => this.deserializeEventRow(row));
1733
+ return Response.json({
1734
+ source: source ?? 'local',
1735
+ eventsReplayed: events.length,
1736
+ events,
1737
+ });
1738
+ }
1739
+ /**
1740
+ * Handle POST /events/rebuild - rebuild entity from events
1741
+ */
1742
+ async handleRebuildEntity(request) {
1743
+ let body;
1744
+ try {
1745
+ body = (await request.json());
1746
+ }
1747
+ catch {
1748
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1749
+ }
1750
+ const { object } = body;
1751
+ if (!object || typeof object !== 'string') {
1752
+ return Response.json({ error: 'object field is required' }, { status: 400 });
1753
+ }
1754
+ // Parse object format: Type/id
1755
+ const [type, id] = object.split('/');
1756
+ if (!type || !id) {
1757
+ return Response.json({ error: 'Invalid object format, expected Type/id' }, { status: 400 });
1758
+ }
1759
+ // Get all events for this object in chronological order
1760
+ const rows = this.sql
1761
+ .exec('SELECT * FROM _events WHERE object = ? ORDER BY timestamp ASC', object)
1762
+ .toArray();
1763
+ if (rows.length === 0) {
1764
+ return Response.json({ error: 'No events found for this object' }, { status: 404 });
1765
+ }
1766
+ // Rebuild the entity by applying events (excluding delete events)
1767
+ // This allows us to restore deleted entities to their state before deletion
1768
+ let entityData = {};
1769
+ let hasData = false;
1770
+ for (const row of rows) {
1771
+ const event = this.deserializeEventRow(row);
1772
+ const eventType = event['event'];
1773
+ if (eventType.endsWith('.created')) {
1774
+ entityData = event['data'] ?? {};
1775
+ hasData = true;
1776
+ }
1777
+ else if (eventType.endsWith('.updated')) {
1778
+ entityData = { ...entityData, ...(event['data'] ?? {}) };
1779
+ hasData = true;
1780
+ }
1781
+ // Skip .deleted events - we want to restore to the state before deletion
1782
+ }
1783
+ if (!hasData) {
1784
+ return Response.json({ error: 'No data events found for this object' }, { status: 400 });
1785
+ }
1786
+ // Recreate the entity
1787
+ const now = new Date().toISOString();
1788
+ const dataJson = JSON.stringify(entityData);
1789
+ // Check if entity already exists
1790
+ const existing = this.sql.exec('SELECT id FROM _data WHERE id = ?', id).toArray();
1791
+ if (existing.length > 0) {
1792
+ // Update existing
1793
+ this.sql.exec('UPDATE _data SET data = ?, updated_at = ? WHERE id = ?', dataJson, now, id);
1794
+ }
1795
+ else {
1796
+ // Insert new
1797
+ this.sql.exec('INSERT INTO _data (id, type, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', id, type, dataJson, now, now);
1798
+ }
1799
+ return Response.json({
1800
+ id,
1801
+ type,
1802
+ data: entityData,
1803
+ rebuilt: true,
1804
+ eventsApplied: rows.length,
1805
+ });
1806
+ }
1807
+ /**
1808
+ * Handle POST /events/subscribe - create subscription
1809
+ */
1810
+ async handleSubscribe(request) {
1811
+ let body;
1812
+ try {
1813
+ body = (await request.json());
1814
+ }
1815
+ catch {
1816
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1817
+ }
1818
+ const { pattern, webhook } = body;
1819
+ if (!pattern || !webhook) {
1820
+ return Response.json({ error: 'pattern and webhook are required' }, { status: 400 });
1821
+ }
1822
+ const id = crypto.randomUUID();
1823
+ const now = new Date().toISOString();
1824
+ this.sql.exec('INSERT INTO _subscriptions (id, pattern, webhook, created_at) VALUES (?, ?, ?, ?)', id, pattern, webhook, now);
1825
+ return Response.json({
1826
+ id,
1827
+ pattern,
1828
+ webhook,
1829
+ created_at: now,
1830
+ });
1831
+ }
1832
+ /**
1833
+ * Handle GET /events/subscriptions - list subscriptions
1834
+ */
1835
+ handleListSubscriptions() {
1836
+ const rows = this.sql.exec('SELECT * FROM _subscriptions ORDER BY created_at ASC').toArray();
1837
+ return Response.json(rows);
1838
+ }
1839
+ /**
1840
+ * Handle DELETE /events/subscriptions/:id - unsubscribe
1841
+ */
1842
+ handleUnsubscribe(id) {
1843
+ const existing = this.sql.exec('SELECT id FROM _subscriptions WHERE id = ?', id).toArray();
1844
+ if (existing.length === 0) {
1845
+ return Response.json({ error: 'Subscription not found' }, { status: 404 });
1846
+ }
1847
+ this.sql.exec('DELETE FROM _subscriptions WHERE id = ?', id);
1848
+ return Response.json({ deleted: true });
1849
+ }
1850
+ /**
1851
+ * Handle GET /events/subscriptions/:id/deliveries - list deliveries
1852
+ */
1853
+ handleListDeliveries(subId) {
1854
+ // Check if subscription exists
1855
+ const existing = this.sql.exec('SELECT id FROM _subscriptions WHERE id = ?', subId).toArray();
1856
+ if (existing.length === 0) {
1857
+ return Response.json({ error: 'Subscription not found' }, { status: 404 });
1858
+ }
1859
+ // In a real implementation, this would return delivery logs
1860
+ // For now, return empty array as deliveries are not persisted
1861
+ return Response.json([]);
1862
+ }
1863
+ // ===========================================================================
1864
+ // Pipeline handlers
1865
+ // ===========================================================================
1866
+ /**
1867
+ * Handle GET /pipeline/status - get pipeline status
1868
+ */
1869
+ handlePipelineStatus() {
1870
+ return Response.json({
1871
+ eventsProcessed: this.pipelineStats.eventsProcessed,
1872
+ batchesSent: this.pipelineStats.batchesSent,
1873
+ bufferSize: this.pipelineBuffer.length,
1874
+ retriesEnabled: this.pipelineConfig.retryEnabled,
1875
+ });
1876
+ }
1877
+ /**
1878
+ * Handle POST /pipeline/flush - flush pipeline buffer to R2
1879
+ */
1880
+ handlePipelineFlush() {
1881
+ this.flushPipeline();
1882
+ return Response.json({ flushed: true, batchesSent: this.pipelineStats.batchesSent });
1883
+ }
1884
+ /**
1885
+ * Flush the pipeline buffer to R2 storage
1886
+ */
1887
+ flushPipeline() {
1888
+ if (this.pipelineBuffer.length === 0)
1889
+ return;
1890
+ // Store events in _pipeline_r2 for simulating R2 storage
1891
+ const now = new Date();
1892
+ const year = now.getUTCFullYear();
1893
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
1894
+ const day = String(now.getUTCDate()).padStart(2, '0');
1895
+ const batchId = crypto.randomUUID();
1896
+ const key = `events/${year}/${month}/${day}/${batchId}.json`;
1897
+ // Create R2 storage table if not exists
1898
+ this.sql.exec(`
1899
+ CREATE TABLE IF NOT EXISTS _pipeline_r2 (
1900
+ key TEXT PRIMARY KEY,
1901
+ data TEXT NOT NULL,
1902
+ created_at TEXT NOT NULL
1903
+ )
1904
+ `);
1905
+ // Store the batch
1906
+ this.sql.exec('INSERT INTO _pipeline_r2 (key, data, created_at) VALUES (?, ?, ?)', key, JSON.stringify(this.pipelineBuffer), now.toISOString());
1907
+ this.pipelineStats.batchesSent++;
1908
+ this.pipelineBuffer = [];
1909
+ }
1910
+ /**
1911
+ * Handle GET /pipeline/r2/list - list R2 objects
1912
+ */
1913
+ handlePipelineR2List() {
1914
+ // Ensure table exists
1915
+ this.sql.exec(`
1916
+ CREATE TABLE IF NOT EXISTS _pipeline_r2 (
1917
+ key TEXT PRIMARY KEY,
1918
+ data TEXT NOT NULL,
1919
+ created_at TEXT NOT NULL
1920
+ )
1921
+ `);
1922
+ const rows = this.sql
1923
+ .exec('SELECT key, created_at FROM _pipeline_r2 ORDER BY created_at ASC')
1924
+ .toArray();
1925
+ return Response.json(rows);
1926
+ }
1927
+ /**
1928
+ * Handle POST /pipeline/config - configure pipeline
1929
+ */
1930
+ async handlePipelineConfig(request) {
1931
+ let body;
1932
+ try {
1933
+ body = (await request.json());
1934
+ }
1935
+ catch {
1936
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1937
+ }
1938
+ if (typeof body['retryEnabled'] === 'boolean') {
1939
+ this.pipelineConfig.retryEnabled = body['retryEnabled'];
1940
+ }
1941
+ if (typeof body['batchSize'] === 'number') {
1942
+ this.pipelineConfig.batchSize = body['batchSize'];
1943
+ }
1944
+ return Response.json(this.pipelineConfig);
1945
+ }
1946
+ /**
1947
+ * Handle POST /pipeline/test-error - test error handling
1948
+ */
1949
+ async handlePipelineTestError(request) {
1950
+ let body;
1951
+ try {
1952
+ body = (await request.json());
1953
+ }
1954
+ catch {
1955
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
1956
+ }
1957
+ if (body['simulateError']) {
1958
+ // Return error but don't crash
1959
+ return Response.json({ error: 'Simulated pipeline error' }, { status: 503 });
1960
+ }
1961
+ return Response.json({ ok: true });
1962
+ }
1963
+ /**
1964
+ * Deserialize an event row from the database
1965
+ */
1966
+ deserializeEventRow(row) {
1967
+ const r = row;
1968
+ return {
1969
+ id: r.id,
1970
+ event: r.event,
1971
+ actor: r.actor,
1972
+ object: r.object,
1973
+ data: r.data ? (typeof r.data === 'string' ? JSON.parse(r.data) : r.data) : null,
1974
+ result: r.result,
1975
+ previousData: r.previous_data
1976
+ ? typeof r.previous_data === 'string'
1977
+ ? JSON.parse(r.previous_data)
1978
+ : r.previous_data
1979
+ : null,
1980
+ timestamp: r.timestamp,
1981
+ };
1982
+ }
1983
+ // ===========================================================================
1984
+ // Helpers
1985
+ // ===========================================================================
1986
+ deserializeDataRow(row) {
1987
+ const r = row;
1988
+ return {
1989
+ id: r.id,
1990
+ type: r.type,
1991
+ data: typeof r.data === 'string' ? JSON.parse(r.data) : r.data,
1992
+ created_at: r.created_at,
1993
+ updated_at: r.updated_at,
1994
+ };
1995
+ }
1996
+ deserializeRelRow(row) {
1997
+ const r = row;
1998
+ return {
1999
+ from_id: r.from_id,
2000
+ relation: r.relation,
2001
+ to_id: r.to_id,
2002
+ metadata: r.metadata
2003
+ ? typeof r.metadata === 'string'
2004
+ ? JSON.parse(r.metadata)
2005
+ : r.metadata
2006
+ : null,
2007
+ created_at: r.created_at,
2008
+ };
2009
+ }
2010
+ // ===========================================================================
2011
+ // Semantic Search Handlers
2012
+ // ===========================================================================
2013
+ /**
2014
+ * Configure embeddings model
2015
+ */
2016
+ async handleConfigureEmbeddings(request) {
2017
+ let body;
2018
+ try {
2019
+ body = (await request.json());
2020
+ }
2021
+ catch {
2022
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2023
+ }
2024
+ if (body['model']) {
2025
+ this.embeddingsConfig.model = body['model'];
2026
+ }
2027
+ return Response.json(this.embeddingsConfig);
2028
+ }
2029
+ /**
2030
+ * List all embeddings with optional type filter
2031
+ * Generates embeddings lazily for entities that don't have them yet
2032
+ */
2033
+ async handleListEmbeddings(url) {
2034
+ const entityType = url.searchParams.get('entity_type');
2035
+ // If type is specified, generate embeddings for entities without them (lazy generation)
2036
+ if (entityType) {
2037
+ const entities = this.sql.exec('SELECT * FROM _data WHERE type = ?', entityType).toArray();
2038
+ for (const row of entities) {
2039
+ const entity = this.deserializeDataRow(row);
2040
+ const existing = this.sql
2041
+ .exec('SELECT id FROM _embeddings WHERE entity_type = ? AND entity_id = ?', entityType, entity['id'])
2042
+ .toArray();
2043
+ if (existing.length === 0) {
2044
+ await this.generateEmbeddingForEntity(entityType, entity['id'], entity['data']);
2045
+ }
2046
+ }
2047
+ }
2048
+ let query = 'SELECT * FROM _embeddings';
2049
+ const params = [];
2050
+ if (entityType) {
2051
+ query += ' WHERE entity_type = ?';
2052
+ params.push(entityType);
2053
+ }
2054
+ query += ' ORDER BY created_at ASC';
2055
+ const rows = this.sql.exec(query, ...params).toArray();
2056
+ const results = rows.map((row) => {
2057
+ const embRow = row;
2058
+ return {
2059
+ entity_type: embRow.entity_type,
2060
+ entity_id: embRow.entity_id,
2061
+ model: embRow.model,
2062
+ vector: typeof embRow.vector === 'string' ? JSON.parse(embRow.vector) : embRow.vector,
2063
+ content_hash: embRow.content_hash,
2064
+ created_at: embRow.created_at,
2065
+ updated_at: embRow.updated_at,
2066
+ };
2067
+ });
2068
+ return Response.json(results);
2069
+ }
2070
+ /**
2071
+ * Get embedding cache stats
2072
+ */
2073
+ handleEmbeddingsStats() {
2074
+ return Response.json(this.embeddingsCacheStats);
2075
+ }
2076
+ /**
2077
+ * Warm up embedding cache for a type
2078
+ */
2079
+ async handleEmbeddingsWarmup(request) {
2080
+ let body;
2081
+ try {
2082
+ body = (await request.json());
2083
+ }
2084
+ catch {
2085
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2086
+ }
2087
+ const { type: rawType } = body;
2088
+ if (!rawType || typeof rawType !== 'string') {
2089
+ return Response.json({ error: 'type field is required' }, { status: 400 });
2090
+ }
2091
+ const type = rawType;
2092
+ // Get all entities of this type
2093
+ const entities = this.sql.exec('SELECT * FROM _data WHERE type = ?', type).toArray();
2094
+ let warmed = 0;
2095
+ for (const row of entities) {
2096
+ const entity = this.deserializeDataRow(row);
2097
+ // Check if embedding already exists
2098
+ const existing = this.sql
2099
+ .exec('SELECT id FROM _embeddings WHERE entity_type = ? AND entity_id = ?', type, entity['id'])
2100
+ .toArray();
2101
+ if (existing.length === 0) {
2102
+ await this.generateEmbeddingForEntity(type, entity['id'], entity['data']);
2103
+ warmed++;
2104
+ }
2105
+ }
2106
+ return Response.json({ warmed });
2107
+ }
2108
+ /**
2109
+ * Generate embeddings for all entities of a type
2110
+ */
2111
+ async handleEmbeddingsGenerate(request) {
2112
+ let body;
2113
+ try {
2114
+ body = (await request.json());
2115
+ }
2116
+ catch {
2117
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2118
+ }
2119
+ const { type: rawType } = body;
2120
+ if (!rawType || typeof rawType !== 'string') {
2121
+ return Response.json({ error: 'type field is required' }, { status: 400 });
2122
+ }
2123
+ const type = rawType;
2124
+ // Get all entities of this type
2125
+ const entities = this.sql.exec('SELECT * FROM _data WHERE type = ?', type).toArray();
2126
+ let generated = 0;
2127
+ for (const row of entities) {
2128
+ const entity = this.deserializeDataRow(row);
2129
+ await this.generateEmbeddingForEntity(type, entity['id'], entity['data']);
2130
+ generated++;
2131
+ }
2132
+ return Response.json({ generated });
2133
+ }
2134
+ /**
2135
+ * Batch process embeddings for specific entities
2136
+ */
2137
+ async handleEmbeddingsBatch(request) {
2138
+ let body;
2139
+ try {
2140
+ body = (await request.json());
2141
+ }
2142
+ catch {
2143
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2144
+ }
2145
+ const { type: rawType, ids, skipExisting } = body;
2146
+ if (!rawType || typeof rawType !== 'string' || !Array.isArray(ids)) {
2147
+ return Response.json({ error: 'type and ids array are required' }, { status: 400 });
2148
+ }
2149
+ const type = rawType;
2150
+ let processed = 0;
2151
+ let skipped = 0;
2152
+ let errors = 0;
2153
+ for (const id of ids) {
2154
+ // Check if entity exists
2155
+ const entityRows = this.sql
2156
+ .exec('SELECT * FROM _data WHERE id = ? AND type = ?', id, type)
2157
+ .toArray();
2158
+ if (entityRows.length === 0) {
2159
+ errors++;
2160
+ continue;
2161
+ }
2162
+ // Check if should skip existing
2163
+ if (skipExisting) {
2164
+ const existing = this.sql
2165
+ .exec('SELECT id FROM _embeddings WHERE entity_type = ? AND entity_id = ?', type, id)
2166
+ .toArray();
2167
+ if (existing.length > 0) {
2168
+ skipped++;
2169
+ continue;
2170
+ }
2171
+ }
2172
+ const entity = this.deserializeDataRow(entityRows[0]);
2173
+ await this.generateEmbeddingForEntity(type, id, entity['data']);
2174
+ processed++;
2175
+ }
2176
+ return Response.json({ processed, skipped, errors, success: true });
2177
+ }
2178
+ /**
2179
+ * Start a batch embedding job
2180
+ */
2181
+ async handleEmbeddingsBatchStart(request) {
2182
+ let body;
2183
+ try {
2184
+ body = (await request.json());
2185
+ }
2186
+ catch {
2187
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2188
+ }
2189
+ const { type: rawType, batchSize = 10 } = body;
2190
+ if (!rawType || typeof rawType !== 'string') {
2191
+ return Response.json({ error: 'type field is required' }, { status: 400 });
2192
+ }
2193
+ const type = rawType;
2194
+ // Count entities of this type
2195
+ const countResult = this.sql
2196
+ .exec('SELECT COUNT(*) as count FROM _data WHERE type = ?', type)
2197
+ .toArray();
2198
+ const countRow = countResult[0];
2199
+ const total = countRow?.count ?? 0;
2200
+ const jobId = crypto.randomUUID();
2201
+ // Store job status
2202
+ this.batchJobs.set(jobId, {
2203
+ status: 'pending',
2204
+ total,
2205
+ processed: 0,
2206
+ errors: 0,
2207
+ });
2208
+ // Start processing in the background
2209
+ this.processBatchJob(jobId, type, batchSize).catch(() => {
2210
+ const job = this.batchJobs.get(jobId);
2211
+ if (job) {
2212
+ job.status = 'failed';
2213
+ }
2214
+ });
2215
+ return Response.json({ jobId, total });
2216
+ }
2217
+ /**
2218
+ * Process a batch job asynchronously
2219
+ */
2220
+ async processBatchJob(jobId, type, batchSize) {
2221
+ const job = this.batchJobs.get(jobId);
2222
+ if (!job)
2223
+ return;
2224
+ job.status = 'processing';
2225
+ // Get all entities of this type
2226
+ const entities = this.sql.exec('SELECT * FROM _data WHERE type = ?', type).toArray();
2227
+ for (let i = 0; i < entities.length; i += batchSize) {
2228
+ const batch = entities.slice(i, i + batchSize);
2229
+ for (const row of batch) {
2230
+ try {
2231
+ const entity = this.deserializeDataRow(row);
2232
+ await this.generateEmbeddingForEntity(type, entity['id'], entity['data']);
2233
+ job.processed++;
2234
+ }
2235
+ catch {
2236
+ job.errors++;
2237
+ }
2238
+ }
2239
+ }
2240
+ job.status = 'completed';
2241
+ }
2242
+ /**
2243
+ * Get batch job status
2244
+ */
2245
+ handleEmbeddingsBatchStatus(jobId) {
2246
+ const job = this.batchJobs.get(jobId);
2247
+ if (!job) {
2248
+ return Response.json({ error: 'Job not found' }, { status: 404 });
2249
+ }
2250
+ return Response.json(job);
2251
+ }
2252
+ /**
2253
+ * Get embedding for a specific entity
2254
+ */
2255
+ async handleGetEmbedding(entityType, entityId) {
2256
+ // Check if entity exists
2257
+ const entityRows = this.sql
2258
+ .exec('SELECT * FROM _data WHERE id = ? AND type = ?', entityId, entityType)
2259
+ .toArray();
2260
+ if (entityRows.length === 0) {
2261
+ return Response.json({ error: 'Entity not found' }, { status: 404 });
2262
+ }
2263
+ // Check if embedding exists
2264
+ const embeddingRows = this.sql
2265
+ .exec('SELECT * FROM _embeddings WHERE entity_type = ? AND entity_id = ?', entityType, entityId)
2266
+ .toArray();
2267
+ if (embeddingRows.length > 0) {
2268
+ // Cache hit
2269
+ this.embeddingsCacheStats.cacheHits++;
2270
+ const embRow = embeddingRows[0];
2271
+ return Response.json({
2272
+ entity_type: embRow.entity_type,
2273
+ entity_id: embRow.entity_id,
2274
+ model: embRow.model,
2275
+ vector: typeof embRow.vector === 'string' ? JSON.parse(embRow.vector) : embRow.vector,
2276
+ content_hash: embRow.content_hash,
2277
+ created_at: embRow.created_at,
2278
+ updated_at: embRow.updated_at,
2279
+ });
2280
+ }
2281
+ // Cache miss - generate embedding
2282
+ this.embeddingsCacheStats.cacheMisses++;
2283
+ const entity = this.deserializeDataRow(entityRows[0]);
2284
+ const embedding = await this.generateEmbeddingForEntity(entityType, entityId, entity.data);
2285
+ if (!embedding) {
2286
+ // Could not generate embedding (e.g., no text content)
2287
+ return Response.json({ error: 'Could not generate embedding' }, { status: 400 });
2288
+ }
2289
+ return Response.json(embedding);
2290
+ }
2291
+ /**
2292
+ * Semantic search using vector similarity
2293
+ */
2294
+ async handleSemanticSearch(request) {
2295
+ let body;
2296
+ try {
2297
+ body = (await request.json());
2298
+ }
2299
+ catch {
2300
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2301
+ }
2302
+ const { type: rawType, query: rawQuery, minScore: rawMinScore = 0, limit: rawLimit = 10, where, since, until, } = body;
2303
+ if (!rawType || typeof rawType !== 'string') {
2304
+ return Response.json({ error: 'type field is required' }, { status: 400 });
2305
+ }
2306
+ if (!rawQuery || typeof rawQuery !== 'string' || rawQuery.trim() === '') {
2307
+ return Response.json({ error: 'query field is required' }, { status: 400 });
2308
+ }
2309
+ const type = rawType;
2310
+ const query = rawQuery;
2311
+ const minScore = rawMinScore;
2312
+ const limit = rawLimit;
2313
+ // Generate embedding for query
2314
+ const queryEmbedding = await this.generateEmbedding(query);
2315
+ // Get all entities of this type and ensure they have embeddings
2316
+ const entities = this.sql.exec('SELECT * FROM _data WHERE type = ?', type).toArray();
2317
+ // Generate embeddings for entities that don't have them yet (lazy generation)
2318
+ for (const row of entities) {
2319
+ const entity = this.deserializeDataRow(row);
2320
+ const existingEmbedding = this.sql
2321
+ .exec('SELECT id FROM _embeddings WHERE entity_type = ? AND entity_id = ?', type, entity['id'])
2322
+ .toArray();
2323
+ if (existingEmbedding.length === 0) {
2324
+ await this.generateEmbeddingForEntity(type, entity['id'], entity['data']);
2325
+ }
2326
+ }
2327
+ // Get all embeddings for this type
2328
+ const embeddingRows = this.sql
2329
+ .exec('SELECT * FROM _embeddings WHERE entity_type = ?', type)
2330
+ .toArray();
2331
+ // Calculate similarity scores
2332
+ const results = [];
2333
+ for (const row of embeddingRows) {
2334
+ const embeddingRow = row;
2335
+ const vector = typeof embeddingRow.vector === 'string'
2336
+ ? JSON.parse(embeddingRow.vector)
2337
+ : embeddingRow.vector;
2338
+ const score = this.cosineSimilarity(queryEmbedding, vector);
2339
+ if (score >= minScore) {
2340
+ results.push({ entityId: embeddingRow.entity_id, score });
2341
+ }
2342
+ }
2343
+ // Sort by score descending
2344
+ results.sort((a, b) => b.score - a.score);
2345
+ // Get entity data for top results
2346
+ const finalResults = [];
2347
+ for (const result of results.slice(0, limit)) {
2348
+ const entityRows = this.sql
2349
+ .exec('SELECT * FROM _data WHERE id = ?', result.entityId)
2350
+ .toArray();
2351
+ if (entityRows.length === 0)
2352
+ continue;
2353
+ const entity = this.deserializeDataRow(entityRows[0]);
2354
+ // Apply where filters if specified
2355
+ if (where) {
2356
+ const data = entity['data'];
2357
+ let matches = true;
2358
+ for (const [field, value] of Object.entries(where)) {
2359
+ if (data[field] !== value) {
2360
+ matches = false;
2361
+ break;
2362
+ }
2363
+ }
2364
+ if (!matches)
2365
+ continue;
2366
+ }
2367
+ // Apply time filters
2368
+ if (since && entity['created_at'] < since)
2369
+ continue;
2370
+ if (until && entity['created_at'] > until)
2371
+ continue;
2372
+ finalResults.push({
2373
+ id: entity['id'],
2374
+ type: entity['type'],
2375
+ data: entity['data'],
2376
+ created_at: entity['created_at'],
2377
+ updated_at: entity['updated_at'],
2378
+ $score: result.score,
2379
+ });
2380
+ }
2381
+ return Response.json(finalResults);
2382
+ }
2383
+ /**
2384
+ * Hybrid search combining FTS and semantic
2385
+ */
2386
+ async handleHybridSearch(request) {
2387
+ let body;
2388
+ try {
2389
+ body = (await request.json());
2390
+ }
2391
+ catch {
2392
+ return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
2393
+ }
2394
+ const { type: rawType, query: rawQuery, limit: rawLimit = 10, ftsWeight: rawFtsWeight = 0.5, semanticWeight: rawSemanticWeight = 0.5, rrfK: rawRrfK = 60, } = body;
2395
+ if (!rawType) {
2396
+ return Response.json({ error: 'type field is required' }, { status: 400 });
2397
+ }
2398
+ if (!rawQuery || rawQuery.trim() === '') {
2399
+ return Response.json({ error: 'query field is required' }, { status: 400 });
2400
+ }
2401
+ const type = rawType;
2402
+ const query = rawQuery;
2403
+ const limit = rawLimit;
2404
+ const ftsWeight = rawFtsWeight;
2405
+ const semanticWeight = rawSemanticWeight;
2406
+ const rrfK = rawRrfK;
2407
+ // Get FTS results
2408
+ const ftsRanks = new Map();
2409
+ const entities = this.sql.exec('SELECT * FROM _data WHERE type = ?', type).toArray();
2410
+ let ftsRank = 1;
2411
+ const queryLower = query.toLowerCase();
2412
+ const queryTerms = queryLower.split(/\s+/);
2413
+ for (const row of entities) {
2414
+ const entity = this.deserializeDataRow(row);
2415
+ const data = entity['data'];
2416
+ // Simple FTS: check if any text field contains query terms
2417
+ let hasMatch = false;
2418
+ for (const value of Object.values(data)) {
2419
+ if (typeof value === 'string') {
2420
+ const valueLower = value.toLowerCase();
2421
+ if (queryTerms.some((term) => valueLower.includes(term))) {
2422
+ hasMatch = true;
2423
+ break;
2424
+ }
2425
+ }
2426
+ }
2427
+ if (hasMatch) {
2428
+ ftsRanks.set(entity['id'], ftsRank++);
2429
+ }
2430
+ }
2431
+ // Get semantic results
2432
+ const semanticRanks = new Map();
2433
+ const semanticScores = new Map();
2434
+ // Ensure embeddings exist for all entities (lazy generation)
2435
+ for (const row of entities) {
2436
+ const entity = this.deserializeDataRow(row);
2437
+ const existingEmbedding = this.sql
2438
+ .exec('SELECT id FROM _embeddings WHERE entity_type = ? AND entity_id = ?', type, entity['id'])
2439
+ .toArray();
2440
+ if (existingEmbedding.length === 0) {
2441
+ await this.generateEmbeddingForEntity(type, entity['id'], entity['data']);
2442
+ }
2443
+ }
2444
+ const queryEmbedding = await this.generateEmbedding(query);
2445
+ const embeddingRows = this.sql
2446
+ .exec('SELECT * FROM _embeddings WHERE entity_type = ?', type)
2447
+ .toArray();
2448
+ const semanticResults = [];
2449
+ for (const row of embeddingRows) {
2450
+ const embeddingRow = row;
2451
+ const vector = typeof embeddingRow.vector === 'string'
2452
+ ? JSON.parse(embeddingRow.vector)
2453
+ : embeddingRow.vector;
2454
+ const score = this.cosineSimilarity(queryEmbedding, vector);
2455
+ semanticResults.push({ entityId: embeddingRow.entity_id, score });
2456
+ semanticScores.set(embeddingRow.entity_id, score);
2457
+ }
2458
+ // Sort by score and assign ranks
2459
+ semanticResults.sort((a, b) => b.score - a.score);
2460
+ let semanticRank = 1;
2461
+ for (const result of semanticResults) {
2462
+ semanticRanks.set(result.entityId, semanticRank++);
2463
+ }
2464
+ // Compute RRF scores
2465
+ const allIds = new Set([...ftsRanks.keys(), ...semanticRanks.keys()]);
2466
+ const rrfResults = [];
2467
+ for (const id of allIds) {
2468
+ const fRank = ftsRanks.get(id) ?? Infinity;
2469
+ const sRank = semanticRanks.get(id) ?? Infinity;
2470
+ const sScore = semanticScores.get(id) ?? 0;
2471
+ const ftsComponent = fRank < Infinity ? ftsWeight / (rrfK + fRank) : 0;
2472
+ const semanticComponent = sRank < Infinity ? semanticWeight / (rrfK + sRank) : 0;
2473
+ const rrfScore = ftsComponent + semanticComponent;
2474
+ rrfResults.push({
2475
+ entityId: id,
2476
+ rrfScore,
2477
+ ftsRank: fRank,
2478
+ semanticRank: sRank,
2479
+ semanticScore: sScore,
2480
+ });
2481
+ }
2482
+ // Sort by RRF score
2483
+ rrfResults.sort((a, b) => b.rrfScore - a.rrfScore);
2484
+ // Get entity data for top results
2485
+ const finalResults = [];
2486
+ for (const result of rrfResults.slice(0, limit)) {
2487
+ const entityRows = this.sql
2488
+ .exec('SELECT * FROM _data WHERE id = ?', result.entityId)
2489
+ .toArray();
2490
+ if (entityRows.length === 0)
2491
+ continue;
2492
+ const entity = this.deserializeDataRow(entityRows[0]);
2493
+ finalResults.push({
2494
+ id: entity['id'],
2495
+ type: entity['type'],
2496
+ data: entity['data'],
2497
+ created_at: entity['created_at'],
2498
+ updated_at: entity['updated_at'],
2499
+ $score: result.semanticScore,
2500
+ $rrfScore: result.rrfScore,
2501
+ $ftsRank: result.ftsRank,
2502
+ $semanticRank: result.semanticRank,
2503
+ });
2504
+ }
2505
+ return Response.json(finalResults);
2506
+ }
2507
+ // ===========================================================================
2508
+ // Embedding Generation Helpers
2509
+ // ===========================================================================
2510
+ /**
2511
+ * Generate embedding for an entity and store it
2512
+ */
2513
+ async generateEmbeddingForEntity(entityType, entityId, data) {
2514
+ // Extract text content from data
2515
+ const text = this.extractEmbeddableText(data);
2516
+ if (!text || text.trim() === '') {
2517
+ // No text content to embed
2518
+ return null;
2519
+ }
2520
+ const contentHash = this.hashContent(text);
2521
+ const now = new Date().toISOString();
2522
+ // Generate embedding using AI binding
2523
+ const vector = await this.generateEmbedding(text);
2524
+ // Upsert into _embeddings table
2525
+ const existing = this.sql
2526
+ .exec('SELECT id, created_at FROM _embeddings WHERE entity_type = ? AND entity_id = ?', entityType, entityId)
2527
+ .toArray();
2528
+ const existingEmb = existing.length > 0 ? existing[0] : null;
2529
+ const id = existingEmb?.id ?? crypto.randomUUID();
2530
+ const createdAt = existingEmb?.created_at ?? now;
2531
+ const vectorJson = JSON.stringify(vector);
2532
+ if (existing.length > 0) {
2533
+ this.sql.exec(`UPDATE _embeddings SET model = ?, vector = ?, content_hash = ?, updated_at = ?
2534
+ WHERE entity_type = ? AND entity_id = ?`, this.embeddingsConfig.model, vectorJson, contentHash, now, entityType, entityId);
2535
+ }
2536
+ else {
2537
+ this.sql.exec(`INSERT INTO _embeddings (id, entity_type, entity_id, model, vector, content_hash, created_at, updated_at)
2538
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, entityType, entityId, this.embeddingsConfig.model, vectorJson, contentHash, now, now);
2539
+ }
2540
+ return {
2541
+ entity_type: entityType,
2542
+ entity_id: entityId,
2543
+ model: this.embeddingsConfig.model,
2544
+ vector,
2545
+ content_hash: contentHash,
2546
+ created_at: createdAt,
2547
+ updated_at: now,
2548
+ };
2549
+ }
2550
+ /**
2551
+ * Generate embedding vector for text using AI binding or fallback
2552
+ */
2553
+ async generateEmbedding(text) {
2554
+ // Try to use AI binding if available
2555
+ const envWithAI = this.env;
2556
+ const ai = envWithAI?.AI;
2557
+ if (ai && typeof ai.run === 'function') {
2558
+ try {
2559
+ const result = await ai.run(this.embeddingsConfig.model, { text: [text] });
2560
+ if (result?.data?.[0]) {
2561
+ return result.data[0];
2562
+ }
2563
+ }
2564
+ catch {
2565
+ // Fall back to deterministic embedding
2566
+ }
2567
+ }
2568
+ // Fallback: generate deterministic embedding based on text content
2569
+ return this.generateDeterministicEmbedding(text);
2570
+ }
2571
+ /**
2572
+ * Generate a deterministic embedding based on text content
2573
+ * This is used as a fallback when no AI binding is available
2574
+ */
2575
+ generateDeterministicEmbedding(text) {
2576
+ // Use semantic word vectors for meaningful similarity
2577
+ const SEMANTIC_VECTORS = {
2578
+ // AI/ML domain
2579
+ machine: [0.9, 0.1, 0.05, 0.02],
2580
+ learning: [0.85, 0.15, 0.08, 0.03],
2581
+ artificial: [0.88, 0.12, 0.06, 0.04],
2582
+ intelligence: [0.87, 0.13, 0.07, 0.05],
2583
+ neural: [0.82, 0.18, 0.09, 0.06],
2584
+ network: [0.75, 0.2, 0.15, 0.1],
2585
+ deep: [0.8, 0.17, 0.1, 0.08],
2586
+ ai: [0.92, 0.08, 0.04, 0.02],
2587
+ ml: [0.88, 0.12, 0.06, 0.03],
2588
+ algorithm: [0.83, 0.17, 0.08, 0.04],
2589
+ algorithms: [0.83, 0.17, 0.08, 0.04],
2590
+ // Programming domain
2591
+ programming: [0.15, 0.85, 0.1, 0.05],
2592
+ code: [0.12, 0.88, 0.12, 0.06],
2593
+ software: [0.18, 0.82, 0.15, 0.08],
2594
+ development: [0.2, 0.8, 0.18, 0.1],
2595
+ typescript: [0.1, 0.9, 0.08, 0.04],
2596
+ javascript: [0.12, 0.88, 0.1, 0.05],
2597
+ python: [0.25, 0.75, 0.12, 0.06],
2598
+ react: [0.08, 0.85, 0.2, 0.1],
2599
+ vue: [0.06, 0.84, 0.18, 0.08],
2600
+ frontend: [0.05, 0.8, 0.25, 0.12],
2601
+ // Database domain
2602
+ database: [0.1, 0.7, 0.08, 0.6],
2603
+ query: [0.12, 0.65, 0.1, 0.7],
2604
+ sql: [0.08, 0.6, 0.05, 0.75],
2605
+ optimization: [0.15, 0.55, 0.12, 0.68],
2606
+ performance: [0.18, 0.5, 0.15, 0.65],
2607
+ // Food domain (very different from tech)
2608
+ cooking: [0.02, 0.05, 0.03, 0.02],
2609
+ recipe: [0.03, 0.04, 0.02, 0.03],
2610
+ recipes: [0.03, 0.04, 0.02, 0.03],
2611
+ food: [0.02, 0.03, 0.02, 0.02],
2612
+ pasta: [0.01, 0.02, 0.01, 0.01],
2613
+ pizza: [0.01, 0.03, 0.02, 0.01],
2614
+ italian: [0.02, 0.04, 0.02, 0.02],
2615
+ traditional: [0.02, 0.03, 0.02, 0.02],
2616
+ // State management - hooks is strongly related to state
2617
+ state: [0.3, 0.5, 0.6, 0.4],
2618
+ management: [0.35, 0.45, 0.55, 0.38],
2619
+ hooks: [0.25, 0.55, 0.65, 0.35],
2620
+ usestate: [0.28, 0.5, 0.62, 0.36],
2621
+ useeffect: [0.24, 0.52, 0.6, 0.34],
2622
+ patterns: [0.3, 0.48, 0.58, 0.37],
2623
+ different: [0.2, 0.4, 0.5, 0.3],
2624
+ // General
2625
+ guide: [0.5, 0.5, 0.5, 0.5],
2626
+ comprehensive: [0.5, 0.5, 0.5, 0.5],
2627
+ introduction: [0.5, 0.5, 0.5, 0.5],
2628
+ overview: [0.5, 0.5, 0.5, 0.5],
2629
+ tutorial: [0.5, 0.5, 0.5, 0.5],
2630
+ tips: [0.5, 0.5, 0.5, 0.5],
2631
+ systems: [0.5, 0.5, 0.5, 0.5],
2632
+ applications: [0.5, 0.5, 0.5, 0.5],
2633
+ // Quantum physics (very different)
2634
+ quantum: [0.01, 0.01, 0.01, 0.99],
2635
+ physics: [0.02, 0.02, 0.01, 0.98],
2636
+ simulation: [0.03, 0.05, 0.02, 0.95],
2637
+ };
2638
+ const DEFAULT_VECTOR = [0.1, 0.1, 0.1, 0.1];
2639
+ // Tokenize
2640
+ const words = text
2641
+ .toLowerCase()
2642
+ .replace(/[^\w\s]/g, ' ')
2643
+ .split(/\s+/)
2644
+ .filter((w) => w.length > 0);
2645
+ if (words.length === 0) {
2646
+ // Return zeros with small noise
2647
+ return Array.from({ length: 768 }, (_, i) => Math.sin(i) * 0.01);
2648
+ }
2649
+ // Aggregate word vectors
2650
+ const aggregated = [0, 0, 0, 0];
2651
+ for (const word of words) {
2652
+ const vec = SEMANTIC_VECTORS[word] ?? DEFAULT_VECTOR;
2653
+ for (let i = 0; i < 4; i++) {
2654
+ aggregated[i] += vec[i];
2655
+ }
2656
+ }
2657
+ // Normalize
2658
+ const norm = Math.sqrt(aggregated.reduce((sum, v) => sum + v * v, 0));
2659
+ const normalized = aggregated.map((v) => v / (norm || 1));
2660
+ // Expand to 768 dimensions
2661
+ const textHash = this.simpleHash(text);
2662
+ const embedding = new Array(768);
2663
+ for (let i = 0; i < 768; i++) {
2664
+ const baseIndex = i % 4;
2665
+ const base = normalized[baseIndex];
2666
+ const noise = this.seededRandom(textHash, i) * 0.1 - 0.05;
2667
+ embedding[i] = base + noise;
2668
+ }
2669
+ // Final normalization
2670
+ const finalNorm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
2671
+ return embedding.map((v) => v / (finalNorm || 1));
2672
+ }
2673
+ /**
2674
+ * Extract text content from entity data for embedding
2675
+ */
2676
+ extractEmbeddableText(data) {
2677
+ const textParts = [];
2678
+ for (const [key, value] of Object.entries(data)) {
2679
+ // Skip internal fields
2680
+ if (key.startsWith('$') || key.startsWith('_'))
2681
+ continue;
2682
+ // Skip timestamps
2683
+ if (key.endsWith('At') || key.endsWith('_at'))
2684
+ continue;
2685
+ if (typeof value === 'string' && value.trim()) {
2686
+ textParts.push(value);
2687
+ }
2688
+ else if (typeof value === 'number') {
2689
+ // Include numbers as text for embedding
2690
+ textParts.push(String(value));
2691
+ }
2692
+ else if (Array.isArray(value)) {
2693
+ const stringValues = value.filter((v) => typeof v === 'string');
2694
+ if (stringValues.length > 0) {
2695
+ textParts.push(stringValues.join(' '));
2696
+ }
2697
+ }
2698
+ }
2699
+ return textParts.join('\n\n');
2700
+ }
2701
+ /**
2702
+ * Calculate cosine similarity between two vectors
2703
+ */
2704
+ cosineSimilarity(a, b) {
2705
+ if (a.length !== b.length) {
2706
+ throw new Error(`Vector dimensions must match: ${a.length} vs ${b.length}`);
2707
+ }
2708
+ let dotProduct = 0;
2709
+ let normA = 0;
2710
+ let normB = 0;
2711
+ for (let i = 0; i < a.length; i++) {
2712
+ dotProduct += a[i] * b[i];
2713
+ normA += a[i] * a[i];
2714
+ normB += b[i] * b[i];
2715
+ }
2716
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
2717
+ if (magnitude === 0)
2718
+ return 0;
2719
+ // Return normalized similarity (0-1 range)
2720
+ return Math.max(0, Math.min(1, (dotProduct / magnitude + 1) / 2));
2721
+ }
2722
+ /**
2723
+ * Simple hash function for deterministic randomness
2724
+ */
2725
+ simpleHash(str) {
2726
+ let hash = 0;
2727
+ for (let i = 0; i < str.length; i++) {
2728
+ const char = str.charCodeAt(i);
2729
+ hash = (hash << 5) - hash + char;
2730
+ hash = hash & hash; // Convert to 32-bit integer
2731
+ }
2732
+ return Math.abs(hash);
2733
+ }
2734
+ /**
2735
+ * Generate deterministic pseudo-random number from seed
2736
+ */
2737
+ seededRandom(seed, index) {
2738
+ const x = Math.sin(seed + index) * 10000;
2739
+ return x - Math.floor(x);
2740
+ }
2741
+ /**
2742
+ * Hash content for change detection
2743
+ */
2744
+ hashContent(text) {
2745
+ const hash = this.simpleHash(text);
2746
+ return hash.toString(16).padStart(8, '0');
2747
+ }
2748
+ }
2749
+ /**
2750
+ * DatabaseService - WorkerEntrypoint for RPC access
2751
+ *
2752
+ * Provides `connect(namespace)` method that returns an RpcTarget service
2753
+ * with all database operations.
2754
+ *
2755
+ * When used standalone (in tests), uses an in-memory provider with namespace isolation.
2756
+ */
2757
+ export class DatabaseService extends WorkerEntrypoint {
2758
+ /**
2759
+ * Connect to a namespace and get an RPC-enabled service
2760
+ *
2761
+ * @param namespace - The namespace to connect to (defaults to 'default')
2762
+ * @param options - Optional provider configuration
2763
+ * @returns DatabaseServiceCore instance for RPC calls
2764
+ */
2765
+ connect(namespace, options) {
2766
+ return new DatabaseServiceCore(namespace ?? 'default', options);
2767
+ }
2768
+ /**
2769
+ * Handle fetch requests - required by vitest-pool-workers for service binding tests
2770
+ * Returns a simple JSON response for health checks or routes to the appropriate service
2771
+ */
2772
+ async fetch(request) {
2773
+ const url = new URL(request.url);
2774
+ // Health check endpoint
2775
+ if (url.pathname === '/health' || url.pathname === '/') {
2776
+ return Response.json({ status: 'ok', service: 'ai-database' });
2777
+ }
2778
+ // For other requests, return 404 - RPC should be used instead
2779
+ return Response.json({ error: 'Not found. Use RPC via service binding instead.' }, { status: 404 });
2780
+ }
2781
+ }
2782
+ // WorkerEntrypoint IS the default export
2783
+ export default DatabaseService;
2784
+ // Export aliases
2785
+ export { DatabaseService as DatabaseWorker };
2786
+ //# sourceMappingURL=worker.js.map