@lota-sdk/core 0.1.5

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 (153) hide show
  1. package/infrastructure/schema/00_workstream.surql +55 -0
  2. package/infrastructure/schema/01_memory.surql +47 -0
  3. package/infrastructure/schema/02_execution_plan.surql +62 -0
  4. package/infrastructure/schema/03_learned_skill.surql +32 -0
  5. package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
  6. package/package.json +128 -0
  7. package/src/ai/definitions.ts +308 -0
  8. package/src/bifrost/bifrost.ts +256 -0
  9. package/src/config/agent-defaults.ts +99 -0
  10. package/src/config/constants.ts +33 -0
  11. package/src/config/env-shapes.ts +122 -0
  12. package/src/config/logger.ts +29 -0
  13. package/src/config/model-constants.ts +31 -0
  14. package/src/config/search.ts +17 -0
  15. package/src/config/workstream-defaults.ts +68 -0
  16. package/src/db/base.service.ts +55 -0
  17. package/src/db/cursor-pagination.ts +73 -0
  18. package/src/db/memory-query-builder.ts +207 -0
  19. package/src/db/memory-store.helpers.ts +118 -0
  20. package/src/db/memory-store.rows.ts +29 -0
  21. package/src/db/memory-store.ts +974 -0
  22. package/src/db/memory-types.ts +193 -0
  23. package/src/db/memory.ts +505 -0
  24. package/src/db/record-id.ts +78 -0
  25. package/src/db/service.ts +932 -0
  26. package/src/db/startup.ts +152 -0
  27. package/src/db/tables.ts +20 -0
  28. package/src/document/org-document-chunking.ts +224 -0
  29. package/src/document/parsing.ts +40 -0
  30. package/src/embeddings/provider.ts +76 -0
  31. package/src/index.ts +302 -0
  32. package/src/queues/context-compaction.queue.ts +82 -0
  33. package/src/queues/document-processor.queue.ts +118 -0
  34. package/src/queues/memory-consolidation.queue.ts +65 -0
  35. package/src/queues/post-chat-memory.queue.ts +128 -0
  36. package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
  37. package/src/queues/regular-chat-memory-digest.config.ts +12 -0
  38. package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
  39. package/src/queues/skill-extraction.config.ts +9 -0
  40. package/src/queues/skill-extraction.queue.ts +62 -0
  41. package/src/redis/connection.ts +176 -0
  42. package/src/redis/index.ts +30 -0
  43. package/src/redis/org-memory-lock.ts +43 -0
  44. package/src/redis/redis-lease-lock.ts +158 -0
  45. package/src/runtime/agent-contract.ts +1 -0
  46. package/src/runtime/agent-prompt-context.ts +119 -0
  47. package/src/runtime/agent-runtime-policy.ts +192 -0
  48. package/src/runtime/agent-stream-helpers.ts +117 -0
  49. package/src/runtime/agent-types.ts +22 -0
  50. package/src/runtime/approval-continuation.ts +16 -0
  51. package/src/runtime/chat-attachments.ts +46 -0
  52. package/src/runtime/chat-message.ts +10 -0
  53. package/src/runtime/chat-request-routing.ts +21 -0
  54. package/src/runtime/chat-run-orchestration.ts +25 -0
  55. package/src/runtime/chat-run-registry.ts +20 -0
  56. package/src/runtime/chat-types.ts +18 -0
  57. package/src/runtime/context-compaction-constants.ts +11 -0
  58. package/src/runtime/context-compaction-runtime.ts +86 -0
  59. package/src/runtime/context-compaction.ts +909 -0
  60. package/src/runtime/execution-plan.ts +59 -0
  61. package/src/runtime/helper-model.ts +405 -0
  62. package/src/runtime/indexed-repositories-policy.ts +28 -0
  63. package/src/runtime/instruction-sections.ts +8 -0
  64. package/src/runtime/llm-content.ts +71 -0
  65. package/src/runtime/memory-block.ts +264 -0
  66. package/src/runtime/memory-digest-policy.ts +14 -0
  67. package/src/runtime/memory-format.ts +8 -0
  68. package/src/runtime/memory-pipeline.ts +570 -0
  69. package/src/runtime/memory-prompts-fact.ts +47 -0
  70. package/src/runtime/memory-prompts-parse.ts +3 -0
  71. package/src/runtime/memory-prompts-update.ts +37 -0
  72. package/src/runtime/memory-scope.ts +43 -0
  73. package/src/runtime/plugin-types.ts +10 -0
  74. package/src/runtime/retrieval-adapters.ts +25 -0
  75. package/src/runtime/retrieval-pipeline.ts +3 -0
  76. package/src/runtime/runtime-extensions.ts +154 -0
  77. package/src/runtime/skill-extraction-policy.ts +3 -0
  78. package/src/runtime/team-consultation-orchestrator.ts +245 -0
  79. package/src/runtime/team-consultation-prompts.ts +32 -0
  80. package/src/runtime/title-helpers.ts +12 -0
  81. package/src/runtime/turn-lifecycle.ts +28 -0
  82. package/src/runtime/workstream-chat-helpers.ts +187 -0
  83. package/src/runtime/workstream-routing-policy.ts +301 -0
  84. package/src/runtime/workstream-state.ts +261 -0
  85. package/src/services/attachment.service.ts +159 -0
  86. package/src/services/chat-attachments.service.ts +17 -0
  87. package/src/services/chat-run-registry.service.ts +3 -0
  88. package/src/services/context-compaction-runtime.ts +13 -0
  89. package/src/services/context-compaction.service.ts +115 -0
  90. package/src/services/document-chunk.service.ts +141 -0
  91. package/src/services/execution-plan.service.ts +890 -0
  92. package/src/services/learned-skill.service.ts +328 -0
  93. package/src/services/memory-assessment.service.ts +43 -0
  94. package/src/services/memory.service.ts +807 -0
  95. package/src/services/memory.utils.ts +84 -0
  96. package/src/services/mutating-approval.service.ts +110 -0
  97. package/src/services/recent-activity-title.service.ts +74 -0
  98. package/src/services/recent-activity.service.ts +397 -0
  99. package/src/services/workstream-change-tracker.service.ts +313 -0
  100. package/src/services/workstream-message.service.ts +283 -0
  101. package/src/services/workstream-title.service.ts +58 -0
  102. package/src/services/workstream-turn-preparation.ts +1340 -0
  103. package/src/services/workstream-turn.ts +37 -0
  104. package/src/services/workstream.service.ts +854 -0
  105. package/src/services/workstream.types.ts +118 -0
  106. package/src/storage/attachment-parser.ts +101 -0
  107. package/src/storage/attachment-storage.service.ts +391 -0
  108. package/src/storage/attachments.types.ts +11 -0
  109. package/src/storage/attachments.utils.ts +58 -0
  110. package/src/storage/generated-document-storage.service.ts +55 -0
  111. package/src/system-agents/agent-result.ts +27 -0
  112. package/src/system-agents/context-compacter.agent.ts +46 -0
  113. package/src/system-agents/delegated-agent-factory.ts +177 -0
  114. package/src/system-agents/helper-agent-options.ts +20 -0
  115. package/src/system-agents/memory-reranker.agent.ts +38 -0
  116. package/src/system-agents/memory.agent.ts +58 -0
  117. package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
  118. package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
  119. package/src/system-agents/researcher.agent.ts +34 -0
  120. package/src/system-agents/skill-extractor.agent.ts +88 -0
  121. package/src/system-agents/skill-manager.agent.ts +80 -0
  122. package/src/system-agents/title-generator.agent.ts +42 -0
  123. package/src/system-agents/workstream-tracker.agent.ts +58 -0
  124. package/src/tools/execution-plan.tool.ts +163 -0
  125. package/src/tools/fetch-webpage.tool.ts +132 -0
  126. package/src/tools/firecrawl-client.ts +12 -0
  127. package/src/tools/memory-block.tool.ts +55 -0
  128. package/src/tools/read-file-parts.tool.ts +80 -0
  129. package/src/tools/remember-memory.tool.ts +85 -0
  130. package/src/tools/research-topic.tool.ts +15 -0
  131. package/src/tools/search-tools.ts +55 -0
  132. package/src/tools/search-web.tool.ts +175 -0
  133. package/src/tools/team-think.tool.ts +125 -0
  134. package/src/tools/tool-contract.ts +21 -0
  135. package/src/tools/user-questions.tool.ts +18 -0
  136. package/src/utils/async.ts +50 -0
  137. package/src/utils/date-time.ts +34 -0
  138. package/src/utils/error.ts +10 -0
  139. package/src/utils/errors.ts +28 -0
  140. package/src/utils/hono-error-handler.ts +71 -0
  141. package/src/utils/string.ts +51 -0
  142. package/src/workers/bootstrap.ts +44 -0
  143. package/src/workers/memory-consolidation.worker.ts +318 -0
  144. package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
  145. package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
  146. package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
  147. package/src/workers/skill-extraction.runner.ts +331 -0
  148. package/src/workers/skill-extraction.worker.ts +22 -0
  149. package/src/workers/utils/repo-indexer-chunker.ts +331 -0
  150. package/src/workers/utils/repo-structure-extractor.ts +645 -0
  151. package/src/workers/utils/repomix-process-concurrency.ts +65 -0
  152. package/src/workers/utils/sandbox-error.ts +5 -0
  153. package/src/workers/worker-utils.ts +182 -0
@@ -0,0 +1,932 @@
1
+ import {
2
+ BoundQuery,
3
+ RecordId,
4
+ ServerError,
5
+ StringRecordId,
6
+ Surreal,
7
+ Table,
8
+ and,
9
+ createRemoteEngines,
10
+ eq,
11
+ } from 'surrealdb'
12
+ import type { ExprLike, Mutation, SurrealTransaction, Values } from 'surrealdb'
13
+ import type { z } from 'zod'
14
+
15
+ import type { RecordIdInput } from './record-id'
16
+ import { ensureRecordId, readCustomStringValue } from './record-id'
17
+ import type { DatabaseTable } from './tables'
18
+
19
+ export class SurrealDBError extends Error {
20
+ constructor(
21
+ message: string,
22
+ public readonly query?: string,
23
+ options?: ErrorOptions,
24
+ ) {
25
+ super(message, options)
26
+ this.name = 'SurrealDBError'
27
+ }
28
+ }
29
+
30
+ export interface SurrealDatabaseConfig {
31
+ url: string
32
+ namespace: string
33
+ database: string
34
+ username?: string
35
+ password?: string
36
+ }
37
+
38
+ export interface SurrealDatabaseLogger {
39
+ info?: (message: string) => void
40
+ warn?: (message: string) => void
41
+ error?: (message: string) => void
42
+ }
43
+
44
+ export type BoundQueryLike = { query: string; bindings?: Record<string, unknown> }
45
+
46
+ interface FindManyOptions {
47
+ limit?: number
48
+ offset?: number
49
+ orderBy?: string
50
+ orderDir?: 'ASC' | 'DESC'
51
+ }
52
+
53
+ type MutationBuilder = {
54
+ content: (data: Record<string, unknown>) => MutationBuilder
55
+ replace: (data: Record<string, unknown>) => MutationBuilder
56
+ patch: (data: Record<string, unknown>) => MutationBuilder
57
+ merge: (data: Record<string, unknown>) => MutationBuilder
58
+ output: (mode: 'after' | 'before') => Promise<unknown>
59
+ }
60
+
61
+ export type CreateMutationBuilder = {
62
+ content: (data: Record<string, unknown>) => CreateMutationBuilder
63
+ output: (mode: 'after' | 'before') => Promise<unknown>
64
+ }
65
+
66
+ export interface DatabaseTransaction {
67
+ query: (query: unknown) => Promise<unknown>
68
+ create: (target: unknown) => CreateMutationBuilder
69
+ relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) => Promise<unknown>
70
+ commit: () => Promise<void>
71
+ cancel: () => Promise<void>
72
+ }
73
+
74
+ function configureMutation(
75
+ builder: MutationBuilder,
76
+ mutation: Mutation,
77
+ data: Record<string, unknown>,
78
+ ): MutationBuilder {
79
+ if (mutation === 'content') {
80
+ return builder.content(data)
81
+ }
82
+ if (mutation === 'replace') {
83
+ return builder.replace(data)
84
+ }
85
+ if (mutation === 'patch') {
86
+ return builder.patch(data)
87
+ }
88
+ return builder.merge(data)
89
+ }
90
+
91
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
92
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
93
+ }
94
+
95
+ function isBoundQueryLike(value: unknown): value is BoundQueryLike {
96
+ if (!isRecordValue(value)) {
97
+ return false
98
+ }
99
+
100
+ if (typeof value.query !== 'string') {
101
+ return false
102
+ }
103
+
104
+ return value.bindings === undefined || isRecordValue(value.bindings)
105
+ }
106
+
107
+ function toStringLikeValue(value: unknown): string | null {
108
+ if (!value || typeof value !== 'object') {
109
+ return null
110
+ }
111
+
112
+ const stringValue = readCustomStringValue(value)
113
+ if (typeof stringValue !== 'string' || stringValue.length === 0 || stringValue === '[object Object]') {
114
+ return null
115
+ }
116
+
117
+ return stringValue
118
+ }
119
+
120
+ const CONNECT_MAX_ATTEMPTS = 5
121
+ const CONNECT_RETRY_BASE_DELAY_MS = 100
122
+ const CONNECT_RETRY_JITTER_MS = 50
123
+ const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000
124
+
125
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
126
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
127
+
128
+ try {
129
+ return await Promise.race([
130
+ promise,
131
+ new Promise<T>((_, reject) => {
132
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs)
133
+ }),
134
+ ])
135
+ } finally {
136
+ if (timeoutId) {
137
+ clearTimeout(timeoutId)
138
+ }
139
+ }
140
+ }
141
+
142
+ export class SurrealDBService {
143
+ private client: Surreal | null = null
144
+ private isConnected = false
145
+ private connectPromise: Promise<void> | null = null
146
+
147
+ constructor(
148
+ private readonly config: SurrealDatabaseConfig,
149
+ private readonly logger?: SurrealDatabaseLogger,
150
+ ) {}
151
+
152
+ private toSurrealError(error: unknown, query?: string): never {
153
+ if (error instanceof SurrealDBError) {
154
+ throw error
155
+ }
156
+
157
+ if (error instanceof ServerError) {
158
+ throw new SurrealDBError(`${error.name}: ${error.message}`, query, { cause: error })
159
+ }
160
+
161
+ if (error instanceof Error) {
162
+ throw new SurrealDBError(error.message, query, { cause: error })
163
+ }
164
+
165
+ throw new SurrealDBError(String(error), query)
166
+ }
167
+
168
+ private isEmbeddedEngine(url: string) {
169
+ return (
170
+ url === 'mem://' ||
171
+ url.startsWith('mem://') ||
172
+ url.startsWith('surrealkv://') ||
173
+ url.startsWith('surrealkv+versioned://') ||
174
+ url.startsWith('indxdb://')
175
+ )
176
+ }
177
+
178
+ private async getOrCreateClient(): Promise<Surreal> {
179
+ if (this.client) {
180
+ return this.client
181
+ }
182
+
183
+ const codecOptions = { useNativeDates: true }
184
+
185
+ if (this.isEmbeddedEngine(this.config.url)) {
186
+ const { createNodeEngines } = await import('@surrealdb/node')
187
+ this.client = new Surreal({
188
+ engines: { ...createRemoteEngines(), ...createNodeEngines() } as NonNullable<
189
+ ConstructorParameters<typeof Surreal>[0]
190
+ >['engines'],
191
+ codecOptions,
192
+ })
193
+ return this.client
194
+ }
195
+
196
+ this.client = new Surreal({ engines: createRemoteEngines(), codecOptions })
197
+ return this.client
198
+ }
199
+
200
+ private async resetClient(): Promise<void> {
201
+ if (!this.client) {
202
+ return
203
+ }
204
+
205
+ try {
206
+ await this.client.close()
207
+ } catch {
208
+ } finally {
209
+ this.client = null
210
+ }
211
+ }
212
+
213
+ private isRetriableConnectConflict(error: unknown): boolean {
214
+ if (!(error instanceof Error)) {
215
+ return false
216
+ }
217
+
218
+ const message = error.message.toLowerCase()
219
+ return (
220
+ message.includes('transaction conflict') ||
221
+ message.includes('transaction read conflict') ||
222
+ message.includes('read or write conflict') ||
223
+ message.includes('write conflict') ||
224
+ message.includes('resource busy') ||
225
+ message.includes('this transaction can be retried')
226
+ )
227
+ }
228
+
229
+ async connect(): Promise<void> {
230
+ if (this.isConnected) {
231
+ return
232
+ }
233
+
234
+ if (this.connectPromise) {
235
+ await this.connectPromise
236
+ return
237
+ }
238
+
239
+ this.connectPromise = (async () => {
240
+ let lastError: unknown = null
241
+
242
+ for (let attempt = 1; attempt <= CONNECT_MAX_ATTEMPTS; attempt += 1) {
243
+ try {
244
+ const client = await this.getOrCreateClient()
245
+
246
+ await withTimeout(
247
+ client.connect(this.config.url, {
248
+ namespace: this.config.namespace,
249
+ database: this.config.database,
250
+ authentication: this.isEmbeddedEngine(this.config.url)
251
+ ? undefined
252
+ : { username: this.config.username ?? '', password: this.config.password ?? '' },
253
+ }),
254
+ CONNECT_ATTEMPT_TIMEOUT_MS,
255
+ `Timed out connecting to SurrealDB at ${this.config.url}`,
256
+ )
257
+
258
+ this.isConnected = true
259
+ this.logger?.info?.('Connected to SurrealDB')
260
+ return
261
+ } catch (error) {
262
+ lastError = error
263
+ this.isConnected = false
264
+ await this.resetClient()
265
+
266
+ const retriable = this.isRetriableConnectConflict(error)
267
+ const hasMoreAttempts = attempt < CONNECT_MAX_ATTEMPTS
268
+ if (!retriable || !hasMoreAttempts) {
269
+ break
270
+ }
271
+
272
+ const backoffMs =
273
+ CONNECT_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1) + Math.floor(Math.random() * CONNECT_RETRY_JITTER_MS)
274
+ this.logger?.warn?.(
275
+ `Retrying SurrealDB connect after retryable transaction conflict (${attempt + 1}/${CONNECT_MAX_ATTEMPTS})`,
276
+ )
277
+ await Bun.sleep(backoffMs)
278
+ }
279
+ }
280
+
281
+ this.toSurrealError(lastError)
282
+ })()
283
+
284
+ try {
285
+ await this.connectPromise
286
+ } finally {
287
+ this.connectPromise = null
288
+ }
289
+ }
290
+
291
+ async disconnect(): Promise<void> {
292
+ if (this.connectPromise) {
293
+ try {
294
+ await this.connectPromise
295
+ } catch (error) {
296
+ this.logger?.warn?.(
297
+ `Disconnect observed failed in-flight connect: ${error instanceof Error ? error.message : String(error)}`,
298
+ )
299
+ return
300
+ }
301
+ }
302
+
303
+ if (!this.isConnected) {
304
+ return
305
+ }
306
+
307
+ this.isConnected = false
308
+
309
+ try {
310
+ await this.client?.close()
311
+ } finally {
312
+ this.client = null
313
+ }
314
+ }
315
+
316
+ private async ensureConnected(): Promise<Surreal> {
317
+ if (this.connectPromise) {
318
+ await this.connectPromise
319
+ }
320
+
321
+ if (this.client === null) {
322
+ throw new SurrealDBError('Database not connected')
323
+ }
324
+
325
+ return this.client
326
+ }
327
+
328
+ private normalizeRecordId(id: unknown, table: DatabaseTable): ReturnType<typeof ensureRecordId> {
329
+ try {
330
+ return ensureRecordId(id as RecordIdInput, table)
331
+ } catch (error) {
332
+ if (error instanceof Error) {
333
+ throw new SurrealDBError(`Invalid record id for table "${table}": ${error.message}`, undefined, {
334
+ cause: error,
335
+ })
336
+ }
337
+ throw new SurrealDBError(`Invalid record id for table "${table}"`)
338
+ }
339
+ }
340
+
341
+ private normalizeQueryRows<T>(statement: unknown, schema?: z.ZodType<T>): T[] {
342
+ if (Array.isArray(statement)) {
343
+ return schema ? statement.map((row) => schema.parse(row)) : (statement as T[])
344
+ }
345
+ if (statement === null || statement === undefined) {
346
+ return []
347
+ }
348
+ return schema ? [schema.parse(statement)] : [statement as T]
349
+ }
350
+
351
+ private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
352
+ const entries = Object.entries(filter)
353
+ if (entries.length === 0) {
354
+ return undefined
355
+ }
356
+
357
+ const expressions = entries.map(([key, value]) => eq(key, this.normalizeRuntimeValue(value)))
358
+ if (expressions.length === 1) {
359
+ return expressions[0]
360
+ }
361
+
362
+ return and(...expressions)
363
+ }
364
+
365
+ private normalizeBoundQuery(query: BoundQuery | BoundQueryLike): BoundQuery {
366
+ if (!(query instanceof BoundQuery) && !isBoundQueryLike(query)) {
367
+ throw new SurrealDBError('Invalid query object: expected a BoundQuery-like value')
368
+ }
369
+
370
+ return new BoundQuery(query.query, this.normalizeBindings(query.bindings))
371
+ }
372
+
373
+ private isSerializedRecordId(value: string): boolean {
374
+ return /^[a-zA-Z][a-zA-Z0-9_]*:(?:⟨.+⟩|.+)$/.test(value)
375
+ }
376
+
377
+ private normalizeRuntimeValue(value: unknown): unknown {
378
+ if (value === null || value === undefined) {
379
+ return value
380
+ }
381
+
382
+ if (
383
+ value instanceof Date ||
384
+ value instanceof RecordId ||
385
+ value instanceof StringRecordId ||
386
+ value instanceof Table
387
+ ) {
388
+ return value
389
+ }
390
+
391
+ if (Array.isArray(value)) {
392
+ return value.map((entry) => this.normalizeRuntimeValue(entry))
393
+ }
394
+
395
+ if (!isRecordValue(value)) {
396
+ return value
397
+ }
398
+
399
+ if ('tb' in value && 'id' in value) {
400
+ return ensureRecordId(value as RecordIdInput)
401
+ }
402
+
403
+ const stringValue = toStringLikeValue(value)
404
+ if (stringValue && this.isSerializedRecordId(stringValue)) {
405
+ return ensureRecordId(stringValue)
406
+ }
407
+
408
+ const entries = Object.entries(value)
409
+ if (entries.length === 0) {
410
+ return value
411
+ }
412
+
413
+ return Object.fromEntries(entries.map(([key, entryValue]) => [key, this.normalizeRuntimeValue(entryValue)]))
414
+ }
415
+
416
+ private normalizeBindings(bindings?: Record<string, unknown>): Record<string, unknown> | undefined {
417
+ if (!bindings) {
418
+ return undefined
419
+ }
420
+
421
+ return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
422
+ }
423
+
424
+ private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
425
+ const normalized = this.normalizeRuntimeValue(data) as Record<string, unknown>
426
+ return this.stripNullValues(normalized)
427
+ }
428
+
429
+ /**
430
+ * SurrealDB v3 `option<T>` fields expect `NONE` (key omitted), not `null`.
431
+ * Strip top-level `null` values so they are omitted from the serialized payload,
432
+ * which the driver interprets as NONE.
433
+ */
434
+ private stripNullValues(data: Record<string, unknown>): Record<string, unknown> {
435
+ const result: Record<string, unknown> = {}
436
+ for (const [key, value] of Object.entries(data)) {
437
+ if (value !== null) {
438
+ result[key] = value
439
+ }
440
+ }
441
+ return result
442
+ }
443
+
444
+ private normalizeTableValue(value: unknown): Table {
445
+ if (value instanceof Table) {
446
+ return value as Table<string>
447
+ }
448
+
449
+ if (typeof value === 'string' && value.length > 0) {
450
+ return new Table(value)
451
+ }
452
+
453
+ const stringValue = toStringLikeValue(value)
454
+ if (stringValue) {
455
+ return new Table(stringValue)
456
+ }
457
+
458
+ throw new SurrealDBError('Invalid table value')
459
+ }
460
+
461
+ private wrapMutationBuilder(builder: MutationBuilder): MutationBuilder {
462
+ return {
463
+ content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
464
+ replace: (data) => this.wrapMutationBuilder(builder.replace(this.normalizeMutationData(data))),
465
+ patch: (data) => this.wrapMutationBuilder(builder.patch(this.normalizeMutationData(data))),
466
+ merge: (data) => this.wrapMutationBuilder(builder.merge(this.normalizeMutationData(data))),
467
+ output: (mode) => builder.output(mode),
468
+ }
469
+ }
470
+
471
+ private wrapCreateBuilder(builder: CreateMutationBuilder): CreateMutationBuilder {
472
+ return {
473
+ content: (data) => this.wrapCreateBuilder(builder.content(this.normalizeMutationData(data))),
474
+ output: (mode) => builder.output(mode),
475
+ }
476
+ }
477
+
478
+ private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
479
+ return {
480
+ query: (query: unknown) => tx.query(this.normalizeBoundQuery(query as BoundQuery | BoundQueryLike)),
481
+ create: (target: unknown) =>
482
+ this.wrapCreateBuilder(tx.create(this.normalizeTableValue(target)) as unknown as CreateMutationBuilder),
483
+ relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
484
+ tx.relate(
485
+ ensureRecordId(from as RecordIdInput),
486
+ this.normalizeTableValue(edgeTable),
487
+ ensureRecordId(to as RecordIdInput),
488
+ data
489
+ ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<Record<string, unknown>>)
490
+ : undefined,
491
+ ),
492
+ commit: () => tx.commit(),
493
+ cancel: () => tx.cancel(),
494
+ }
495
+ }
496
+
497
+ private resolveQueryText(query: BoundQuery | BoundQueryLike): string {
498
+ return query.query
499
+ }
500
+
501
+ async query<T>(query: BoundQuery | BoundQueryLike): Promise<T[]> {
502
+ const statements = await this.queryAll<T>(query)
503
+ return statements.at(0) ?? []
504
+ }
505
+
506
+ async queryAll<T>(query: BoundQuery | BoundQueryLike, schema?: z.ZodType<T>): Promise<T[][]> {
507
+ const client = await this.ensureConnected()
508
+ const boundQuery = this.normalizeBoundQuery(query)
509
+ const queryText = this.resolveQueryText(boundQuery)
510
+
511
+ try {
512
+ const responses = await client.query(boundQuery).responses()
513
+
514
+ return responses.map((response, index) => {
515
+ if (!response.success) {
516
+ const failure = response.error
517
+ throw new SurrealDBError(`Statement ${index + 1}: ${failure.message}`, queryText, { cause: failure })
518
+ }
519
+ return this.normalizeQueryRows<T>(response.result, schema)
520
+ })
521
+ } catch (error) {
522
+ this.toSurrealError(error, queryText)
523
+ }
524
+ }
525
+
526
+ async queryOne<T extends z.ZodType>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T> | null> {
527
+ const results = await this.query<unknown>(query)
528
+ const first = results.at(0)
529
+ return first ? schema.parse(first) : null
530
+ }
531
+
532
+ async queryMany<T extends z.ZodType>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T>[]> {
533
+ const results = await this.query<unknown>(query)
534
+ return results.map((row) => schema.parse(row))
535
+ }
536
+
537
+ private async runSqlFile(file: Bun.BunFile): Promise<void> {
538
+ const sql = (await file.text()).trim()
539
+ if (!sql) {
540
+ return
541
+ }
542
+
543
+ await this.queryAll<unknown>(new BoundQuery(sql))
544
+ }
545
+
546
+ async applySchemaAndMigrations(schemaFiles: readonly Bun.BunFile[]): Promise<void> {
547
+ for (const schemaFile of schemaFiles) {
548
+ await this.runSqlFile(schemaFile)
549
+ }
550
+ }
551
+
552
+ async findOne<T extends z.ZodType>(
553
+ table: DatabaseTable,
554
+ filter: Record<string, unknown>,
555
+ schema: T,
556
+ ): Promise<z.infer<T> | null> {
557
+ const client = await this.ensureConnected()
558
+ const selection = this.buildFilterExpression(filter)
559
+
560
+ try {
561
+ let query = client.select<unknown>(new Table(table))
562
+ if (selection) {
563
+ query = query.where(selection)
564
+ }
565
+
566
+ const rows = await query.limit(1)
567
+ const first = rows.at(0)
568
+ return first ? schema.parse(first) : null
569
+ } catch (error) {
570
+ this.toSurrealError(error, `SELECT * FROM ${table} LIMIT 1`)
571
+ }
572
+ }
573
+
574
+ async findMany<T extends z.ZodType>(
575
+ table: DatabaseTable,
576
+ filter: Record<string, unknown>,
577
+ schema: T,
578
+ options?: FindManyOptions,
579
+ ): Promise<z.infer<T>[]> {
580
+ const filterKeys = Object.keys(filter)
581
+
582
+ const client = await this.ensureConnected()
583
+ const selection = this.buildFilterExpression(filter)
584
+ const orderBy = options?.orderBy
585
+
586
+ if (options && orderBy) {
587
+ const { orderDir = 'ASC', limit, offset } = options
588
+ const vars: Record<string, unknown> = this.normalizeMutationData(filter)
589
+ let sql = `SELECT * FROM ${table}`
590
+ if (filterKeys.length > 0) {
591
+ const conditions = filterKeys.map((key) => `${key} = $${key}`).join(' AND ')
592
+ sql += ` WHERE ${conditions}`
593
+ }
594
+ sql += ` ORDER BY ${orderBy} ${orderDir}`
595
+ if (limit !== undefined) {
596
+ sql += ' LIMIT $limitParam'
597
+ vars.limitParam = limit
598
+ }
599
+ if (offset !== undefined) {
600
+ sql += ' START $offsetParam'
601
+ vars.offsetParam = offset
602
+ }
603
+ const rows = await this.query<unknown>(new BoundQuery(sql, vars))
604
+ return rows.map((row) => schema.parse(row))
605
+ }
606
+
607
+ try {
608
+ let query = client.select<unknown>(new Table(table))
609
+ if (selection) {
610
+ query = query.where(selection)
611
+ }
612
+ if (options?.offset !== undefined) {
613
+ query = query.start(options.offset)
614
+ }
615
+ if (options?.limit !== undefined) {
616
+ query = query.limit(options.limit)
617
+ }
618
+
619
+ const rows = await query
620
+ return rows.map((row) => schema.parse(row))
621
+ } catch (error) {
622
+ this.toSurrealError(error, `SELECT * FROM ${table}`)
623
+ }
624
+ }
625
+
626
+ async create<T extends z.ZodType>(
627
+ table: DatabaseTable,
628
+ data: Record<string, unknown>,
629
+ schema: T,
630
+ ): Promise<z.infer<T>> {
631
+ const keys = Object.keys(data)
632
+ if (keys.length === 0) {
633
+ throw new SurrealDBError(`Cannot create record in ${table} with empty data`)
634
+ }
635
+
636
+ const client = await this.ensureConnected()
637
+
638
+ try {
639
+ const created = await client
640
+ .create<unknown>(new Table(table))
641
+ .content(this.normalizeMutationData(data))
642
+ .output('after')
643
+ const first = Array.isArray(created) ? created.at(0) : created
644
+
645
+ if (!first) {
646
+ throw new SurrealDBError(`Failed to create record in ${table}`)
647
+ }
648
+
649
+ return schema.parse(first)
650
+ } catch (error) {
651
+ this.toSurrealError(error, `CREATE ${table}`)
652
+ }
653
+ }
654
+
655
+ async createWithId<T extends z.ZodType>(
656
+ table: DatabaseTable,
657
+ id: unknown,
658
+ data: Record<string, unknown>,
659
+ schema: T,
660
+ ): Promise<z.infer<T>> {
661
+ const keys = Object.keys(data)
662
+ if (keys.length === 0) {
663
+ throw new SurrealDBError(`Cannot create record in ${table} with empty data`)
664
+ }
665
+
666
+ const recordId = this.normalizeRecordId(id, table)
667
+ const client = await this.ensureConnected()
668
+
669
+ try {
670
+ const created = await client.create<unknown>(recordId).content(this.normalizeMutationData(data)).output('after')
671
+ return schema.parse(created)
672
+ } catch (error) {
673
+ this.toSurrealError(error, `CREATE ${recordId.toString()}`)
674
+ }
675
+ }
676
+
677
+ async update<T extends z.ZodType>(
678
+ table: DatabaseTable,
679
+ id: unknown,
680
+ data: Record<string, unknown>,
681
+ schema: T,
682
+ options?: { mutation?: Mutation },
683
+ ): Promise<z.infer<T> | null> {
684
+ const recordId = this.normalizeRecordId(id, table)
685
+
686
+ const keys = Object.keys(data)
687
+ if (keys.length === 0) {
688
+ throw new SurrealDBError('Cannot update record with empty data')
689
+ }
690
+
691
+ const client = await this.ensureConnected()
692
+ const mutation = options?.mutation ?? 'merge'
693
+
694
+ try {
695
+ const builder = client.update<unknown>(recordId)
696
+ const configured = configureMutation(builder, mutation, this.normalizeMutationData(data))
697
+ const updated = await configured.output('after')
698
+ return updated ? schema.parse(updated) : null
699
+ } catch (error) {
700
+ this.toSurrealError(error, `UPDATE ${recordId.toString()}`)
701
+ }
702
+ }
703
+
704
+ async upsert<T extends z.ZodType>(
705
+ table: DatabaseTable,
706
+ id: unknown,
707
+ data: Record<string, unknown>,
708
+ schema: T,
709
+ options?: { mutation?: Mutation },
710
+ ): Promise<z.infer<T>> {
711
+ const recordId = this.normalizeRecordId(id, table)
712
+ const keys = Object.keys(data)
713
+ if (keys.length === 0) {
714
+ throw new SurrealDBError('Cannot upsert record with empty data')
715
+ }
716
+
717
+ const client = await this.ensureConnected()
718
+ const mutation = options?.mutation ?? 'merge'
719
+
720
+ try {
721
+ const builder = client.upsert<unknown>(recordId)
722
+ const configured = configureMutation(builder, mutation, this.normalizeMutationData(data))
723
+ const upserted = await configured.output('after')
724
+ if (!upserted) {
725
+ throw new SurrealDBError(`Failed to upsert record in ${table}`)
726
+ }
727
+ return schema.parse(upserted)
728
+ } catch (error) {
729
+ this.toSurrealError(error, `UPSERT ${recordId.toString()}`)
730
+ }
731
+ }
732
+
733
+ async deleteById(table: DatabaseTable, id: unknown): Promise<boolean> {
734
+ const recordId = this.normalizeRecordId(id, table)
735
+ const client = await this.ensureConnected()
736
+
737
+ try {
738
+ const matched = await client.select<{ id: unknown }>(new Table(table)).where(eq('id', recordId)).limit(1)
739
+ if (matched.length === 0) {
740
+ return false
741
+ }
742
+ await client.delete<unknown>(recordId).output('before')
743
+ return true
744
+ } catch (error) {
745
+ this.toSurrealError(error, `DELETE ${recordId.toString()}`)
746
+ }
747
+ }
748
+
749
+ async deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): Promise<number> {
750
+ const filterKeys = Object.keys(filter)
751
+ if (filterKeys.length === 0) {
752
+ throw new SurrealDBError(`Refusing to delete all records in ${table} without a filter`)
753
+ }
754
+
755
+ const selection = this.buildFilterExpression(filter)
756
+ if (!selection) {
757
+ throw new SurrealDBError(`Invalid delete filter for table ${table}`)
758
+ }
759
+
760
+ const client = await this.ensureConnected()
761
+
762
+ try {
763
+ const matched = await client.select<{ id: unknown }>(new Table(table)).where(selection)
764
+ if (matched.length === 0) {
765
+ return 0
766
+ }
767
+
768
+ for (const row of matched) {
769
+ const id = this.normalizeRecordId(row.id, table)
770
+ await client.delete(id)
771
+ }
772
+
773
+ return matched.length
774
+ } catch (error) {
775
+ this.toSurrealError(error, `DELETE ${table} WHERE ...`)
776
+ }
777
+ }
778
+
779
+ async updateWhere(table: DatabaseTable, where: ExprLike, data: Record<string, unknown>): Promise<number> {
780
+ if (!where) {
781
+ throw new SurrealDBError(`Refusing to update records in ${table} without a where clause`)
782
+ }
783
+ const keys = Object.keys(data)
784
+ if (keys.length === 0) {
785
+ throw new SurrealDBError(`Cannot update records in ${table} with empty data`)
786
+ }
787
+
788
+ const client = await this.ensureConnected()
789
+
790
+ try {
791
+ const updated = await client
792
+ .update<unknown>(new Table(table))
793
+ .where(where)
794
+ .merge(this.normalizeMutationData(data))
795
+ .output('after')
796
+ if (Array.isArray(updated)) {
797
+ return updated.length
798
+ }
799
+ return 1
800
+ } catch (error) {
801
+ this.toSurrealError(error, `UPDATE ${table} WHERE ...`)
802
+ }
803
+ }
804
+
805
+ async insert<T extends Record<string, unknown>>(
806
+ table: DatabaseTable,
807
+ data: Values<T> | Array<Values<T>>,
808
+ ): Promise<T[]> {
809
+ const client = await this.ensureConnected()
810
+
811
+ try {
812
+ const normalized = Array.isArray(data)
813
+ ? data.map((item) => this.normalizeMutationData(item as Record<string, unknown>))
814
+ : this.normalizeMutationData(data as Record<string, unknown>)
815
+ const inserted = await client
816
+ .insert<T>(new Table(table), normalized as Values<T> | Array<Values<T>>)
817
+ .output('after')
818
+ return inserted as T[]
819
+ } catch (error) {
820
+ this.toSurrealError(error, `INSERT ${table}`)
821
+ }
822
+ }
823
+
824
+ async relate<T extends Record<string, unknown>>(
825
+ from: RecordIdInput,
826
+ edgeTable: DatabaseTable,
827
+ to: RecordIdInput,
828
+ data?: Values<T>,
829
+ ): Promise<T | null> {
830
+ const client = await this.ensureConnected()
831
+ const fromRef = ensureRecordId(from)
832
+ const toRef = ensureRecordId(to)
833
+
834
+ try {
835
+ const normalizedData = data
836
+ ? (this.normalizeMutationData(data as Record<string, unknown>) as Values<T>)
837
+ : undefined
838
+ const related = (await client.relate<T>(fromRef, new Table(edgeTable), toRef, normalizedData).output('after')) as
839
+ | T
840
+ | T[]
841
+ | null
842
+ if (related === null) {
843
+ return null
844
+ }
845
+ if (Array.isArray(related)) {
846
+ return related.at(0) ?? null
847
+ }
848
+ return related
849
+ } catch (error) {
850
+ this.toSurrealError(error, `RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`)
851
+ }
852
+ }
853
+
854
+ async beginTransaction(): Promise<DatabaseTransaction> {
855
+ const client = await this.ensureConnected()
856
+ try {
857
+ return this.wrapTransaction(await client.beginTransaction())
858
+ } catch (error) {
859
+ this.toSurrealError(error, 'BEGIN TRANSACTION')
860
+ }
861
+ }
862
+
863
+ async withTransaction<T>(work: (tx: DatabaseTransaction) => Promise<T>): Promise<T> {
864
+ const tx = await this.beginTransaction()
865
+ try {
866
+ const result = await work(tx)
867
+ await tx.commit()
868
+ return result
869
+ } catch (error) {
870
+ try {
871
+ await tx.cancel()
872
+ } catch (cancelError) {
873
+ this.logger?.warn?.(
874
+ `Failed to cancel transaction after error: ${
875
+ cancelError instanceof Error ? cancelError.message : String(cancelError)
876
+ }`,
877
+ )
878
+ }
879
+ throw error
880
+ }
881
+ }
882
+ }
883
+
884
+ let currentDatabaseService: SurrealDBService | undefined
885
+ const databaseServiceOverrides = new Map<PropertyKey, unknown>()
886
+
887
+ function bindTargetMethod(
888
+ target: Record<PropertyKey, unknown>,
889
+ value: (...args: unknown[]) => unknown,
890
+ ): (...args: unknown[]) => unknown {
891
+ return (...args) => Reflect.apply(value, target, args)
892
+ }
893
+
894
+ function resolveConfiguredDatabaseService(): SurrealDBService {
895
+ if (!currentDatabaseService) {
896
+ throw new SurrealDBError('Database service not configured')
897
+ }
898
+
899
+ return currentDatabaseService
900
+ }
901
+
902
+ export const databaseService = new Proxy({} as SurrealDBService, {
903
+ get(_target, property) {
904
+ if (databaseServiceOverrides.has(property)) {
905
+ return databaseServiceOverrides.get(property)
906
+ }
907
+
908
+ const target = resolveConfiguredDatabaseService() as unknown as Record<PropertyKey, unknown>
909
+ const value = target[property]
910
+ if (typeof value === 'function') {
911
+ return bindTargetMethod(target, value as (...args: unknown[]) => unknown)
912
+ }
913
+
914
+ return value
915
+ },
916
+ set(_target, property, value) {
917
+ databaseServiceOverrides.set(property, value)
918
+ return true
919
+ },
920
+ })
921
+
922
+ export function setDatabaseService(db: SurrealDBService): void {
923
+ if (db === databaseService) {
924
+ return
925
+ }
926
+
927
+ currentDatabaseService = db
928
+ }
929
+
930
+ export function getDatabaseService(): SurrealDBService | undefined {
931
+ return currentDatabaseService
932
+ }