@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.
- package/infrastructure/schema/00_workstream.surql +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- 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
|
+
}
|