@lota-sdk/core 0.4.9 → 0.4.10
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/package.json +2 -2
- package/src/ai/embedding-cache.ts +3 -1
- package/src/ai-gateway/ai-gateway.ts +38 -10
- package/src/config/agent-defaults.ts +22 -9
- package/src/config/agent-types.ts +1 -1
- package/src/config/background-processing.ts +1 -1
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +20 -7
- package/src/config/thread-defaults.ts +12 -4
- package/src/create-runtime.ts +69 -656
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.ts +29 -20
- package/src/db/memory.ts +188 -195
- package/src/db/service-normalization.ts +97 -64
- package/src/db/service.ts +706 -538
- package/src/db/startup.ts +30 -19
- package/src/effect/awaitable-effect.ts +46 -37
- package/src/effect/helpers.ts +30 -5
- package/src/effect/index.ts +7 -5
- package/src/effect/layers.ts +82 -72
- package/src/effect/runtime.ts +18 -3
- package/src/effect/services.ts +15 -11
- package/src/embeddings/provider.ts +65 -66
- package/src/index.ts +13 -11
- package/src/queues/autonomous-job.queue.ts +59 -71
- package/src/queues/context-compaction.queue.ts +6 -18
- package/src/queues/delayed-node-promotion.queue.ts +9 -17
- package/src/queues/organization-learning.queue.ts +17 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +23 -20
- package/src/queues/plan-scheduler.queue.ts +6 -18
- package/src/queues/post-chat-memory.queue.ts +6 -18
- package/src/queues/queue-factory.ts +128 -50
- package/src/queues/title-generation.queue.ts +6 -17
- package/src/redis/connection.ts +181 -164
- package/src/redis/runtime-connection.ts +13 -3
- package/src/redis/stream-context.ts +17 -9
- package/src/runtime/agent-runtime-policy.ts +1 -1
- package/src/runtime/agent-stream-helpers.ts +15 -11
- package/src/runtime/chat-run-orchestration.ts +1 -1
- package/src/runtime/context-compaction/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction/context-compaction.ts +126 -82
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/graph-designer.ts +15 -7
- package/src/runtime/helper-model.ts +8 -4
- package/src/runtime/index.ts +0 -1
- package/src/runtime/memory/memory-block.ts +19 -9
- package/src/runtime/memory/memory-pipeline.ts +53 -66
- package/src/runtime/memory/memory-scope.ts +33 -29
- package/src/runtime/plugin-resolution.ts +33 -54
- package/src/runtime/post-turn-side-effects.ts +6 -26
- package/src/runtime/retrieval-adapters.ts +4 -4
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +3 -3
- package/src/runtime/runtime-extensions.ts +20 -9
- package/src/runtime/runtime-lifecycle.ts +124 -0
- package/src/runtime/runtime-services.ts +386 -0
- package/src/runtime/runtime-token.ts +47 -0
- package/src/runtime/social-chat/social-chat-agent-runner.ts +7 -5
- package/src/runtime/social-chat/social-chat-history.ts +21 -12
- package/src/runtime/social-chat/social-chat.ts +401 -365
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +58 -52
- package/src/runtime/thread-turn-context.ts +21 -27
- package/src/services/agent-activity.service.ts +1 -1
- package/src/services/agent-executor.service.ts +179 -187
- package/src/services/artifact.service.ts +10 -5
- package/src/services/attachment.service.ts +35 -1
- package/src/services/autonomous-job.service.ts +58 -56
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +3 -1
- package/src/services/context-compaction.service.ts +1 -1
- package/src/services/document-chunk.service.ts +8 -17
- package/src/services/execution-plan/execution-plan-graph.ts +74 -52
- package/src/services/execution-plan/execution-plan.service.ts +1 -1
- package/src/services/feedback-loop.service.ts +1 -1
- package/src/services/global-orchestrator.service.ts +33 -10
- package/src/services/graph-full-routing.ts +44 -33
- package/src/services/index.ts +1 -0
- package/src/services/institutional-memory.service.ts +8 -17
- package/src/services/learned-skill.service.ts +38 -35
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +14 -3
- package/src/services/memory/memory-preseeded.ts +10 -4
- package/src/services/memory/memory-utils.ts +2 -1
- package/src/services/memory/memory.service.ts +26 -44
- package/src/services/memory/rerank.service.ts +3 -11
- package/src/services/monitoring-window.service.ts +1 -1
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +2 -2
- package/src/services/notification.service.ts +16 -4
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/organization.service.ts +34 -51
- package/src/services/ownership-dispatcher.service.ts +132 -90
- package/src/services/plan/plan-agent-heartbeat.service.ts +1 -1
- package/src/services/plan/plan-agent-query.service.ts +1 -1
- package/src/services/plan/plan-approval.service.ts +52 -48
- package/src/services/plan/plan-artifact.service.ts +2 -2
- package/src/services/plan/plan-builder.service.ts +2 -2
- package/src/services/plan/plan-checkpoint.service.ts +1 -1
- package/src/services/plan/plan-compiler.service.ts +1 -1
- package/src/services/plan/plan-completion-side-effects.ts +18 -24
- package/src/services/plan/plan-coordination.service.ts +1 -1
- package/src/services/plan/plan-cycle.service.ts +171 -164
- package/src/services/plan/plan-deadline.service.ts +290 -304
- package/src/services/plan/plan-event-delivery.service.ts +44 -39
- package/src/services/plan/plan-executor-graph.ts +114 -67
- package/src/services/plan/plan-executor-helpers.ts +60 -75
- package/src/services/plan/plan-executor.service.ts +550 -467
- package/src/services/plan/plan-run.service.ts +12 -19
- package/src/services/plan/plan-scheduler.service.ts +27 -33
- package/src/services/plan/plan-template.service.ts +1 -1
- package/src/services/plan/plan-transaction-events.ts +8 -5
- package/src/services/plan/plan-validator.service.ts +1 -1
- package/src/services/plan/plan-workspace.service.ts +17 -11
- package/src/services/plugin-executor.service.ts +26 -21
- package/src/services/quality-metrics.service.ts +1 -1
- package/src/services/queue-job.service.ts +8 -17
- package/src/services/recent-activity-title.service.ts +17 -9
- package/src/services/recent-activity.service.ts +1 -1
- package/src/services/skill-resolver.service.ts +1 -1
- package/src/services/social-chat-history.service.ts +37 -20
- package/src/services/system-executor.service.ts +25 -20
- package/src/services/thread/thread-bootstrap.ts +26 -10
- package/src/services/thread/thread-listing.ts +2 -1
- package/src/services/thread/thread-memory-block.ts +18 -5
- package/src/services/thread/thread-message.service.ts +24 -8
- package/src/services/thread/thread-title.service.ts +1 -1
- package/src/services/thread/thread-turn-execution.ts +1 -1
- package/src/services/thread/thread-turn-preparation.service.ts +18 -16
- package/src/services/thread/thread-turn-streaming.ts +12 -11
- package/src/services/thread/thread-turn.ts +43 -10
- package/src/services/thread/thread.service.ts +11 -2
- package/src/services/user.service.ts +1 -1
- package/src/services/write-intent-validator.service.ts +1 -1
- package/src/storage/attachment-storage.service.ts +7 -4
- package/src/storage/generated-document-storage.service.ts +1 -1
- package/src/system-agents/context-compaction.agent.ts +1 -1
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/skill-extractor.agent.ts +1 -1
- package/src/system-agents/skill-manager.agent.ts +1 -1
- package/src/system-agents/title-generator.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +28 -17
- package/src/tools/fetch-webpage.tool.ts +20 -13
- package/src/tools/firecrawl-client.ts +13 -3
- package/src/tools/plan-approval.tool.ts +9 -1
- package/src/tools/search-web.tool.ts +16 -9
- package/src/tools/team-think.tool.ts +2 -2
- package/src/utils/async.ts +15 -6
- package/src/utils/errors.ts +27 -15
- package/src/workers/bootstrap.ts +25 -48
- package/src/workers/organization-learning.worker.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +25 -15
- package/src/workers/worker-utils.ts +20 -2
- package/src/config/search.ts +0 -3
- package/src/runtime/agent-types.ts +0 -1
package/src/db/service.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Duration, Effect, Schedule } from 'effect'
|
|
1
|
+
import { Duration, Effect, Schedule, Semaphore } from 'effect'
|
|
2
2
|
import { BoundQuery, ServerError, Surreal, Table, createRemoteEngines } from 'surrealdb'
|
|
3
3
|
import type { ExprLike, SurrealTransaction, Values } from 'surrealdb'
|
|
4
4
|
import { ZodError } from 'zod'
|
|
@@ -8,8 +8,8 @@ import { serverLogger } from '../config/logger'
|
|
|
8
8
|
import type { AwaitableEffect } from '../effect/awaitable-effect'
|
|
9
9
|
import { toAwaitableEffect } from '../effect/awaitable-effect'
|
|
10
10
|
import { getErrorMessage } from '../utils/errors'
|
|
11
|
-
import type { RecordIdInput } from './record-id'
|
|
12
|
-
import {
|
|
11
|
+
import type { RecordIdInput, ensureRecordId } from './record-id'
|
|
12
|
+
import { ensureRecordIdEffect } from './record-id'
|
|
13
13
|
import {
|
|
14
14
|
assertValidIdentifier,
|
|
15
15
|
buildBoundFilterClauses,
|
|
@@ -71,6 +71,8 @@ type CreateBuilderSource = {
|
|
|
71
71
|
output: (mode: 'after' | 'before') => unknown
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
type PendingMutation = { mutation: RecordMutation; data: Record<string, unknown> }
|
|
75
|
+
|
|
74
76
|
export type CreateMutationBuilder = {
|
|
75
77
|
content: (data: Record<string, unknown>) => CreateMutationBuilder
|
|
76
78
|
output: (mode: 'after' | 'before') => AwaitableEffect<unknown, SurrealDBError>
|
|
@@ -106,22 +108,29 @@ function isRetriableConnectError(error: unknown): boolean {
|
|
|
106
108
|
)
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
function isSurrealDBError(error: unknown): error is SurrealDBError {
|
|
112
|
+
return typeof error === 'object' && error !== null && (error as { _tag?: unknown })._tag === 'SurrealDBError'
|
|
113
|
+
}
|
|
114
|
+
|
|
109
115
|
export class SurrealDBService {
|
|
110
116
|
private client: Surreal | null = null
|
|
111
117
|
private isConnected = false
|
|
118
|
+
// Single-permit semaphore acts as a mutex so only one connect attempt runs at a time.
|
|
119
|
+
// Subsequent waiters re-check `isConnected` after acquiring the permit and become no-ops.
|
|
120
|
+
private readonly connectMutex = Semaphore.makeUnsafe(1)
|
|
112
121
|
|
|
113
122
|
constructor(
|
|
114
123
|
private readonly config: SurrealDatabaseConfig,
|
|
115
124
|
private readonly logger?: SurrealDatabaseLogger,
|
|
116
125
|
) {}
|
|
117
126
|
|
|
118
|
-
private toSurrealError(error: unknown, query?: string): SurrealDBError
|
|
119
|
-
if (error
|
|
127
|
+
private toSurrealError(error: unknown, query?: string): SurrealDBError {
|
|
128
|
+
if (isSurrealDBError(error)) {
|
|
120
129
|
return error
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
if (error instanceof ZodError) {
|
|
124
|
-
return error
|
|
133
|
+
return new SurrealDBError({ message: error.message, query: query, cause: error })
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
if (error instanceof ServerError) {
|
|
@@ -151,33 +160,34 @@ export class SurrealDBService {
|
|
|
151
160
|
|
|
152
161
|
const codecOptions = { useNativeDates: true }
|
|
153
162
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
return Effect.gen(
|
|
164
|
+
function* (this: SurrealDBService) {
|
|
165
|
+
if (this.client) {
|
|
166
|
+
return this.client
|
|
167
|
+
}
|
|
159
168
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
169
|
+
if (this.isEmbeddedEngine(this.config.url)) {
|
|
170
|
+
const { createNodeEngines } = yield* Effect.tryPromise({
|
|
171
|
+
try: () => import('@surrealdb/node'),
|
|
172
|
+
catch: (error) =>
|
|
173
|
+
new SurrealDBError({
|
|
174
|
+
message: `Failed to load embedded SurrealDB engine: ${getErrorMessage(error)}`,
|
|
175
|
+
cause: error,
|
|
176
|
+
}),
|
|
177
|
+
})
|
|
178
|
+
this.client = new Surreal({
|
|
179
|
+
engines: { ...createRemoteEngines(), ...createNodeEngines() } as NonNullable<
|
|
180
|
+
ConstructorParameters<typeof Surreal>[0]
|
|
181
|
+
>['engines'],
|
|
182
|
+
codecOptions,
|
|
183
|
+
})
|
|
184
|
+
return this.client
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.client = new Surreal({ engines: createRemoteEngines(), codecOptions })
|
|
188
|
+
return this.client
|
|
189
|
+
}.bind(this),
|
|
190
|
+
)
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
private resetClient(): Effect.Effect<void, never> {
|
|
@@ -207,83 +217,110 @@ export class SurrealDBService {
|
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
connect(): AwaitableEffect<void, SurrealDBError> {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
return toAwaitableEffect(
|
|
221
|
+
Effect.gen(
|
|
222
|
+
function* (this: SurrealDBService) {
|
|
223
|
+
// Fast path: already connected. Cheap, runs inside the Effect so callers
|
|
224
|
+
// observe the same memory barrier the slow path relies on.
|
|
225
|
+
if (this.isConnected) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
213
228
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
namespace: this.config.namespace,
|
|
220
|
-
database: this.config.database,
|
|
221
|
-
authentication: this.isEmbeddedEngine(this.config.url)
|
|
222
|
-
? undefined
|
|
223
|
-
: { username: this.config.username ?? '', password: this.config.password ?? '' },
|
|
224
|
-
}),
|
|
225
|
-
catch: (error) =>
|
|
226
|
-
new SurrealDBError({
|
|
227
|
-
message: `Failed to connect to SurrealDB (${this.config.url}): ${getErrorMessage(error)}`,
|
|
228
|
-
cause: error,
|
|
229
|
-
}),
|
|
230
|
-
}).pipe(
|
|
231
|
-
Effect.timeout(Duration.millis(CONNECT_ATTEMPT_TIMEOUT_MS)),
|
|
232
|
-
Effect.catchTag('TimeoutError', () =>
|
|
233
|
-
Effect.fail(new SurrealDBError({ message: `Timed out connecting to SurrealDB (${this.config.url})` })),
|
|
234
|
-
),
|
|
235
|
-
),
|
|
236
|
-
),
|
|
237
|
-
Effect.tap(() =>
|
|
238
|
-
Effect.sync(() => {
|
|
239
|
-
this.isConnected = true
|
|
240
|
-
this.logger?.info?.('Connected to SurrealDB')
|
|
241
|
-
}),
|
|
242
|
-
),
|
|
243
|
-
Effect.tapError(() =>
|
|
244
|
-
Effect.sync(() => {
|
|
245
|
-
this.isConnected = false
|
|
246
|
-
}).pipe(Effect.andThen(this.resetClient())),
|
|
229
|
+
// Slow path: serialize concurrent connect attempts behind a single permit
|
|
230
|
+
// so only one client.connect() ever runs. Late waiters double-check
|
|
231
|
+
// isConnected after acquiring the permit and become no-ops.
|
|
232
|
+
yield* this.connectMutex.withPermits(1)(this.connectLockedEffect())
|
|
233
|
+
}.bind(this),
|
|
247
234
|
),
|
|
248
|
-
Effect.retry({
|
|
249
|
-
times: CONNECT_MAX_ATTEMPTS - 1,
|
|
250
|
-
schedule: Schedule.jittered(Schedule.exponential(Duration.millis(CONNECT_RETRY_BASE_DELAY_MS), 2)),
|
|
251
|
-
while: isRetriableConnectError,
|
|
252
|
-
}),
|
|
253
|
-
Effect.asVoid,
|
|
254
235
|
)
|
|
255
|
-
|
|
256
|
-
return toAwaitableEffect(connectEffect)
|
|
257
236
|
}
|
|
258
237
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (!self.isConnected) {
|
|
238
|
+
private connectLockedEffect(): Effect.Effect<void, SurrealDBError> {
|
|
239
|
+
return Effect.gen(
|
|
240
|
+
function* (this: SurrealDBService) {
|
|
241
|
+
if (this.isConnected) {
|
|
264
242
|
return
|
|
265
243
|
}
|
|
266
244
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
245
|
+
const connectEffect = this.getOrCreateClient().pipe(
|
|
246
|
+
Effect.flatMap((client) =>
|
|
247
|
+
Effect.tryPromise({
|
|
248
|
+
try: () =>
|
|
249
|
+
client.connect(this.config.url, {
|
|
250
|
+
namespace: this.config.namespace,
|
|
251
|
+
database: this.config.database,
|
|
252
|
+
authentication: this.isEmbeddedEngine(this.config.url)
|
|
253
|
+
? undefined
|
|
254
|
+
: { username: this.config.username ?? '', password: this.config.password ?? '' },
|
|
255
|
+
}),
|
|
256
|
+
catch: (error) =>
|
|
257
|
+
new SurrealDBError({
|
|
258
|
+
message: `Failed to connect to SurrealDB (${this.config.url}): ${getErrorMessage(error)}`,
|
|
259
|
+
cause: error,
|
|
260
|
+
}),
|
|
261
|
+
}).pipe(
|
|
262
|
+
Effect.timeout(Duration.millis(CONNECT_ATTEMPT_TIMEOUT_MS)),
|
|
263
|
+
Effect.catchTag('TimeoutError', () =>
|
|
264
|
+
Effect.fail(new SurrealDBError({ message: `Timed out connecting to SurrealDB (${this.config.url})` })),
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
Effect.tap(() =>
|
|
281
269
|
Effect.sync(() => {
|
|
282
|
-
|
|
270
|
+
this.isConnected = true
|
|
271
|
+
this.logger?.info?.('Connected to SurrealDB')
|
|
283
272
|
}),
|
|
284
273
|
),
|
|
274
|
+
Effect.tapError(() =>
|
|
275
|
+
Effect.sync(() => {
|
|
276
|
+
this.isConnected = false
|
|
277
|
+
}).pipe(Effect.andThen(this.resetClient())),
|
|
278
|
+
),
|
|
279
|
+
Effect.retry({
|
|
280
|
+
times: CONNECT_MAX_ATTEMPTS - 1,
|
|
281
|
+
schedule: Schedule.jittered(Schedule.exponential(Duration.millis(CONNECT_RETRY_BASE_DELAY_MS), 2)),
|
|
282
|
+
while: isRetriableConnectError,
|
|
283
|
+
}),
|
|
284
|
+
Effect.asVoid,
|
|
285
285
|
)
|
|
286
|
-
|
|
286
|
+
|
|
287
|
+
yield* connectEffect
|
|
288
|
+
}.bind(this),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
disconnect(): AwaitableEffect<void, SurrealDBError> {
|
|
293
|
+
return toAwaitableEffect(
|
|
294
|
+
Effect.gen(
|
|
295
|
+
function* (this: SurrealDBService) {
|
|
296
|
+
if (!this.isConnected) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.isConnected = false
|
|
301
|
+
|
|
302
|
+
const client = this.client
|
|
303
|
+
if (!client) {
|
|
304
|
+
this.client = null
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
yield* Effect.tryPromise({
|
|
309
|
+
try: () => client.close(),
|
|
310
|
+
catch: (error) =>
|
|
311
|
+
new SurrealDBError({
|
|
312
|
+
message: `Failed to close database client: ${getErrorMessage(error)}`,
|
|
313
|
+
cause: error,
|
|
314
|
+
}),
|
|
315
|
+
}).pipe(
|
|
316
|
+
Effect.ensuring(
|
|
317
|
+
Effect.sync(() => {
|
|
318
|
+
this.client = null
|
|
319
|
+
}),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
}.bind(this),
|
|
323
|
+
),
|
|
287
324
|
)
|
|
288
325
|
}
|
|
289
326
|
|
|
@@ -292,7 +329,7 @@ export class SurrealDBService {
|
|
|
292
329
|
return this.connect().pipe(
|
|
293
330
|
Effect.flatMap(() => this.getOrCreateClient()),
|
|
294
331
|
Effect.mapError((error) =>
|
|
295
|
-
error
|
|
332
|
+
isSurrealDBError(error)
|
|
296
333
|
? error
|
|
297
334
|
: new SurrealDBError({ message: 'Database not connected', query, cause: error }),
|
|
298
335
|
),
|
|
@@ -302,7 +339,10 @@ export class SurrealDBService {
|
|
|
302
339
|
return Effect.succeed(this.client)
|
|
303
340
|
}
|
|
304
341
|
|
|
305
|
-
private
|
|
342
|
+
private normalizeRecordIdEffect(
|
|
343
|
+
id: unknown,
|
|
344
|
+
table: DatabaseTable,
|
|
345
|
+
): Effect.Effect<ReturnType<typeof ensureRecordId>, SurrealDBError> {
|
|
306
346
|
return normalizeRecordIdForTable(id, table)
|
|
307
347
|
}
|
|
308
348
|
|
|
@@ -310,14 +350,35 @@ export class SurrealDBService {
|
|
|
310
350
|
return normalizeQueryRows(statement, schema, (schemaValue, value) => this.parseSchema(schemaValue, value))
|
|
311
351
|
}
|
|
312
352
|
|
|
353
|
+
private normalizeQueryRowsEffect(
|
|
354
|
+
statement: unknown,
|
|
355
|
+
schema?: z.ZodTypeAny,
|
|
356
|
+
): Effect.Effect<unknown[], SurrealDBError> {
|
|
357
|
+
return Effect.try({
|
|
358
|
+
try: () => this.normalizeQueryRows(statement, schema),
|
|
359
|
+
catch: (error) => this.toSurrealError(error),
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
313
363
|
private normalizeParseValue(value: unknown): unknown {
|
|
314
364
|
return normalizeSurrealValue(value)
|
|
315
365
|
}
|
|
316
366
|
|
|
367
|
+
private normalizeParseValueEffect(value: unknown): Effect.Effect<unknown, SurrealDBError> {
|
|
368
|
+
return Effect.try({ try: () => this.normalizeParseValue(value), catch: (error) => this.toSurrealError(error) })
|
|
369
|
+
}
|
|
370
|
+
|
|
317
371
|
private parseSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> {
|
|
318
372
|
return schema.parse(this.normalizeParseValue(value))
|
|
319
373
|
}
|
|
320
374
|
|
|
375
|
+
private parseSchemaEffect<TSchema extends z.ZodTypeAny>(
|
|
376
|
+
schema: TSchema,
|
|
377
|
+
value: unknown,
|
|
378
|
+
): Effect.Effect<z.infer<TSchema>, SurrealDBError> {
|
|
379
|
+
return Effect.try({ try: () => this.parseSchema(schema, value), catch: (error) => this.toSurrealError(error) })
|
|
380
|
+
}
|
|
381
|
+
|
|
321
382
|
private parseOptionalSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> | null {
|
|
322
383
|
if (value === null || value === undefined) {
|
|
323
384
|
return null
|
|
@@ -326,22 +387,43 @@ export class SurrealDBService {
|
|
|
326
387
|
return this.parseSchema(schema, value)
|
|
327
388
|
}
|
|
328
389
|
|
|
390
|
+
private parseOptionalSchemaEffect<TSchema extends z.ZodTypeAny>(
|
|
391
|
+
schema: TSchema,
|
|
392
|
+
value: unknown,
|
|
393
|
+
): Effect.Effect<z.infer<TSchema> | null, SurrealDBError> {
|
|
394
|
+
return Effect.try({
|
|
395
|
+
try: () => this.parseOptionalSchema(schema, value),
|
|
396
|
+
catch: (error) => this.toSurrealError(error),
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
329
400
|
private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
|
|
330
401
|
return buildFilterExpression(filter)
|
|
331
402
|
}
|
|
332
403
|
|
|
333
|
-
private
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
404
|
+
private buildFilterExpressionEffect(
|
|
405
|
+
filter: Record<string, unknown>,
|
|
406
|
+
): Effect.Effect<ExprLike | undefined, SurrealDBError> {
|
|
407
|
+
return Effect.try({ try: () => this.buildFilterExpression(filter), catch: (error) => this.toSurrealError(error) })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private buildBoundFilterClausesEffect(
|
|
411
|
+
filter: Record<string, unknown>,
|
|
412
|
+
): Effect.Effect<{ clause: string; bindings: Record<string, unknown> }, SurrealDBError> {
|
|
337
413
|
return buildBoundFilterClauses(filter)
|
|
338
414
|
}
|
|
339
415
|
|
|
340
|
-
private
|
|
416
|
+
private assertValidIdentifierEffect(name: string, context: string): Effect.Effect<void, SurrealDBError> {
|
|
417
|
+
return assertValidIdentifier(name, context)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private normalizeBoundQueryEffect<T extends unknown[] = unknown[]>(
|
|
421
|
+
query: BoundQuery<T>,
|
|
422
|
+
): Effect.Effect<BoundQuery<T>, SurrealDBError> {
|
|
341
423
|
return normalizeBoundQuery(query)
|
|
342
424
|
}
|
|
343
425
|
|
|
344
|
-
private
|
|
426
|
+
private normalizeTransactionQueryEffect(query: unknown): Effect.Effect<BoundQuery, SurrealDBError> {
|
|
345
427
|
return normalizeTransactionQuery(query)
|
|
346
428
|
}
|
|
347
429
|
|
|
@@ -349,114 +431,176 @@ export class SurrealDBService {
|
|
|
349
431
|
return normalizeMutationData(data)
|
|
350
432
|
}
|
|
351
433
|
|
|
352
|
-
private
|
|
434
|
+
private normalizeMutationDataEffect(
|
|
435
|
+
data: Record<string, unknown>,
|
|
436
|
+
): Effect.Effect<Record<string, unknown>, SurrealDBError> {
|
|
437
|
+
return Effect.try({ try: () => this.normalizeMutationData(data), catch: (error) => this.toSurrealError(error) })
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private normalizeTableValueEffect(value: unknown): Effect.Effect<Table<string>, SurrealDBError> {
|
|
353
441
|
return normalizeTableValue(value)
|
|
354
442
|
}
|
|
355
443
|
|
|
356
|
-
private
|
|
444
|
+
private normalizeCreateTargetEffect(
|
|
445
|
+
value: unknown,
|
|
446
|
+
): Effect.Effect<Table<string> | ReturnType<typeof ensureRecordId>, SurrealDBError> {
|
|
357
447
|
return normalizeCreateTarget(value)
|
|
358
448
|
}
|
|
359
449
|
|
|
360
|
-
private
|
|
450
|
+
private normalizeTransactionRecordIdEffect(
|
|
451
|
+
value: unknown,
|
|
452
|
+
context: string,
|
|
453
|
+
): Effect.Effect<ReturnType<typeof ensureRecordId>, SurrealDBError> {
|
|
454
|
+
return normalizeTransactionRecordId(value, context)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private wrapMutationBuilder(
|
|
458
|
+
builderEffect: Effect.Effect<MutationBuilderSource, SurrealDBError>,
|
|
459
|
+
pendingMutation?: PendingMutation,
|
|
460
|
+
): MutationBuilder {
|
|
361
461
|
return {
|
|
362
|
-
content: (data) => this.wrapMutationBuilder(
|
|
363
|
-
replace: (data) => this.wrapMutationBuilder(
|
|
364
|
-
merge: (data) => this.wrapMutationBuilder(
|
|
462
|
+
content: (data) => this.wrapMutationBuilder(builderEffect, { mutation: 'content', data }),
|
|
463
|
+
replace: (data) => this.wrapMutationBuilder(builderEffect, { mutation: 'replace', data }),
|
|
464
|
+
merge: (data) => this.wrapMutationBuilder(builderEffect, { mutation: 'merge', data }),
|
|
365
465
|
output: (mode) =>
|
|
366
466
|
toAwaitableEffect(
|
|
367
|
-
Effect.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
467
|
+
Effect.gen(
|
|
468
|
+
function* (this: SurrealDBService) {
|
|
469
|
+
const builder = yield* builderEffect
|
|
470
|
+
let configuredBuilder = builder
|
|
471
|
+
if (pendingMutation) {
|
|
472
|
+
const normalizedData = yield* this.normalizeMutationDataEffect(pendingMutation.data)
|
|
473
|
+
configuredBuilder = configureMutation(builder, pendingMutation.mutation, normalizedData)
|
|
474
|
+
}
|
|
475
|
+
const value = yield* Effect.tryPromise({
|
|
476
|
+
try: () => Promise.resolve(configuredBuilder.output(mode)),
|
|
477
|
+
catch: (error) =>
|
|
478
|
+
new SurrealDBError({
|
|
479
|
+
message: `Failed to finish mutation output: ${getErrorMessage(error)}`,
|
|
480
|
+
cause: error,
|
|
481
|
+
}),
|
|
482
|
+
})
|
|
483
|
+
return yield* this.normalizeParseValueEffect(value)
|
|
484
|
+
}.bind(this),
|
|
485
|
+
),
|
|
375
486
|
),
|
|
376
487
|
}
|
|
377
488
|
}
|
|
378
489
|
|
|
379
|
-
private wrapCreateBuilder(
|
|
490
|
+
private wrapCreateBuilder(
|
|
491
|
+
builderEffect: Effect.Effect<CreateBuilderSource, SurrealDBError>,
|
|
492
|
+
pendingData?: Record<string, unknown>,
|
|
493
|
+
): CreateMutationBuilder {
|
|
380
494
|
return {
|
|
381
|
-
content: (data) => this.wrapCreateBuilder(
|
|
495
|
+
content: (data) => this.wrapCreateBuilder(builderEffect, data),
|
|
382
496
|
output: (mode) =>
|
|
383
497
|
toAwaitableEffect(
|
|
384
|
-
Effect.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
498
|
+
Effect.gen(
|
|
499
|
+
function* (this: SurrealDBService) {
|
|
500
|
+
const builder = yield* builderEffect
|
|
501
|
+
const configuredBuilder = pendingData
|
|
502
|
+
? builder.content(yield* this.normalizeMutationDataEffect(pendingData))
|
|
503
|
+
: builder
|
|
504
|
+
const value = yield* Effect.tryPromise({
|
|
505
|
+
try: () => Promise.resolve(configuredBuilder.output(mode)),
|
|
506
|
+
catch: (error) =>
|
|
507
|
+
new SurrealDBError({
|
|
508
|
+
message: `Failed to finish create output: ${getErrorMessage(error)}`,
|
|
509
|
+
cause: error,
|
|
510
|
+
}),
|
|
511
|
+
})
|
|
512
|
+
return yield* this.normalizeParseValueEffect(value)
|
|
513
|
+
}.bind(this),
|
|
514
|
+
),
|
|
392
515
|
),
|
|
393
516
|
}
|
|
394
517
|
}
|
|
395
518
|
|
|
396
519
|
private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
|
|
397
|
-
const self = this
|
|
398
520
|
return {
|
|
399
|
-
query: (query: unknown) =>
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
521
|
+
query: (query: unknown) =>
|
|
522
|
+
toAwaitableEffect(
|
|
523
|
+
Effect.gen(
|
|
524
|
+
function* (this: SurrealDBService) {
|
|
525
|
+
const boundQuery = yield* this.normalizeTransactionQueryEffect(query)
|
|
526
|
+
const queryText = this.resolveQueryText(boundQuery)
|
|
527
|
+
const responses = yield* Effect.tryPromise({
|
|
528
|
+
try: () => tx.query(boundQuery).responses(),
|
|
529
|
+
catch: (error) =>
|
|
530
|
+
new SurrealDBError({
|
|
531
|
+
message: `Failed to run transaction query: ${getErrorMessage(error)}`,
|
|
532
|
+
query: queryText,
|
|
533
|
+
cause: error,
|
|
534
|
+
}),
|
|
535
|
+
})
|
|
536
|
+
const first = responses.at(0)
|
|
537
|
+
if (!first) {
|
|
538
|
+
return []
|
|
539
|
+
}
|
|
540
|
+
if (!first.success) {
|
|
541
|
+
return yield* new SurrealDBError({ message: first.error.message, query: queryText, cause: first.error })
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return yield* this.normalizeQueryRowsEffect(first.result)
|
|
545
|
+
}.bind(this),
|
|
546
|
+
),
|
|
547
|
+
),
|
|
548
|
+
create: (target: unknown) =>
|
|
549
|
+
this.wrapCreateBuilder(
|
|
550
|
+
Effect.gen(
|
|
551
|
+
function* (this: SurrealDBService) {
|
|
552
|
+
const normalizedTarget = yield* this.normalizeCreateTargetEffect(target)
|
|
553
|
+
return normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
|
|
554
|
+
}.bind(this),
|
|
555
|
+
),
|
|
556
|
+
),
|
|
431
557
|
update: (target: unknown) =>
|
|
432
|
-
|
|
558
|
+
this.wrapMutationBuilder(
|
|
559
|
+
Effect.gen(
|
|
560
|
+
function* (this: SurrealDBService) {
|
|
561
|
+
const recordId = yield* this.normalizeTransactionRecordIdEffect(target, 'transaction update')
|
|
562
|
+
return tx.update(recordId)
|
|
563
|
+
}.bind(this),
|
|
564
|
+
),
|
|
565
|
+
),
|
|
433
566
|
delete: (target: unknown) =>
|
|
434
567
|
toAwaitableEffect(
|
|
435
|
-
Effect.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
568
|
+
Effect.gen(
|
|
569
|
+
function* (this: SurrealDBService) {
|
|
570
|
+
const recordId = yield* this.normalizeTransactionRecordIdEffect(target, 'transaction delete')
|
|
571
|
+
const value = yield* Effect.tryPromise({
|
|
572
|
+
try: () => tx.delete(recordId),
|
|
573
|
+
catch: (error) =>
|
|
574
|
+
new SurrealDBError({
|
|
575
|
+
message: `Failed to delete transaction target: ${getErrorMessage(error)}`,
|
|
576
|
+
cause: error,
|
|
577
|
+
}),
|
|
578
|
+
})
|
|
579
|
+
return yield* this.normalizeParseValueEffect(value)
|
|
580
|
+
}.bind(this),
|
|
581
|
+
),
|
|
443
582
|
),
|
|
444
583
|
relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
|
|
445
584
|
toAwaitableEffect(
|
|
446
|
-
Effect.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
585
|
+
Effect.gen(
|
|
586
|
+
function* (this: SurrealDBService) {
|
|
587
|
+
const fromRecordId = yield* this.normalizeTransactionRecordIdEffect(from, 'transaction relate source')
|
|
588
|
+
const normalizedEdgeTable = yield* this.normalizeTableValueEffect(edgeTable)
|
|
589
|
+
const toRecordId = yield* this.normalizeTransactionRecordIdEffect(to, 'transaction relate target')
|
|
590
|
+
const normalizedData = data
|
|
591
|
+
? yield* this.normalizeMutationDataEffect(data as Record<string, unknown>)
|
|
592
|
+
: undefined
|
|
593
|
+
const value = yield* Effect.tryPromise({
|
|
594
|
+
try: () => tx.relate(fromRecordId, normalizedEdgeTable, toRecordId, normalizedData),
|
|
595
|
+
catch: (error) =>
|
|
596
|
+
new SurrealDBError({
|
|
597
|
+
message: `Failed to relate transaction records: ${getErrorMessage(error)}`,
|
|
598
|
+
cause: error,
|
|
599
|
+
}),
|
|
600
|
+
})
|
|
601
|
+
return yield* this.normalizeParseValueEffect(value)
|
|
602
|
+
}.bind(this),
|
|
603
|
+
),
|
|
460
604
|
),
|
|
461
605
|
commit: () =>
|
|
462
606
|
toAwaitableEffect(
|
|
@@ -482,86 +626,93 @@ export class SurrealDBService {
|
|
|
482
626
|
}
|
|
483
627
|
|
|
484
628
|
query<T>(query: BoundQuery): AwaitableEffect<T[], SurrealDBError> {
|
|
485
|
-
const self = this
|
|
486
629
|
return toAwaitableEffect(
|
|
487
|
-
Effect.gen(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
630
|
+
Effect.gen(
|
|
631
|
+
function* (this: SurrealDBService) {
|
|
632
|
+
const boundQuery = yield* this.normalizeBoundQueryEffect(query)
|
|
633
|
+
const statements = yield* this.queryAll<T>(boundQuery)
|
|
634
|
+
return statements.at(0) ?? []
|
|
635
|
+
}.bind(this),
|
|
636
|
+
),
|
|
492
637
|
)
|
|
493
638
|
}
|
|
494
639
|
|
|
495
640
|
queryAll<T>(query: BoundQuery, schema?: z.ZodTypeAny): AwaitableEffect<T[][], SurrealDBError> {
|
|
496
|
-
const self = this
|
|
497
641
|
return toAwaitableEffect(
|
|
498
|
-
Effect.gen(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
cause: error,
|
|
509
|
-
}),
|
|
510
|
-
})
|
|
511
|
-
return yield* Effect.forEach(responses, (response, index) =>
|
|
512
|
-
Effect.gen(function* () {
|
|
513
|
-
if (!response.success) {
|
|
514
|
-
const failure = response.error
|
|
515
|
-
return yield* new SurrealDBError({
|
|
516
|
-
message: `Statement ${index + 1}: ${failure.message}`,
|
|
642
|
+
Effect.gen(
|
|
643
|
+
function* (this: SurrealDBService) {
|
|
644
|
+
const boundQuery = yield* this.normalizeBoundQueryEffect(query)
|
|
645
|
+
const queryText = this.resolveQueryText(boundQuery)
|
|
646
|
+
const client = yield* this.ensureConnectedEffect(queryText)
|
|
647
|
+
const responses = yield* Effect.tryPromise({
|
|
648
|
+
try: () => client.query(boundQuery).responses(),
|
|
649
|
+
catch: (error) =>
|
|
650
|
+
new SurrealDBError({
|
|
651
|
+
message: `Failed to run query: ${getErrorMessage(error)}`,
|
|
517
652
|
query: queryText,
|
|
518
|
-
cause:
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
653
|
+
cause: error,
|
|
654
|
+
}),
|
|
655
|
+
})
|
|
656
|
+
return yield* Effect.forEach(responses, (response, index) =>
|
|
657
|
+
Effect.gen(
|
|
658
|
+
function* (this: SurrealDBService) {
|
|
659
|
+
if (!response.success) {
|
|
660
|
+
const failure = response.error
|
|
661
|
+
return yield* new SurrealDBError({
|
|
662
|
+
message: `Statement ${index + 1}: ${failure.message}`,
|
|
663
|
+
query: queryText,
|
|
664
|
+
cause: failure,
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
return (yield* this.normalizeQueryRowsEffect(response.result, schema)) as T[]
|
|
668
|
+
}.bind(this),
|
|
669
|
+
),
|
|
670
|
+
)
|
|
671
|
+
}.bind(this),
|
|
672
|
+
),
|
|
525
673
|
)
|
|
526
674
|
}
|
|
527
675
|
|
|
528
676
|
queryOne<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
|
|
529
|
-
const self = this
|
|
530
677
|
return toAwaitableEffect(
|
|
531
|
-
Effect.gen(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
678
|
+
Effect.gen(
|
|
679
|
+
function* (this: SurrealDBService) {
|
|
680
|
+
const boundQuery = yield* this.normalizeBoundQueryEffect(query)
|
|
681
|
+
const results = yield* this.query<unknown>(boundQuery)
|
|
682
|
+
const first = results.at(0)
|
|
683
|
+
return first ? yield* this.parseSchemaEffect(schema, first) : null
|
|
684
|
+
}.bind(this),
|
|
685
|
+
),
|
|
537
686
|
)
|
|
538
687
|
}
|
|
539
688
|
|
|
540
689
|
queryMany<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): AwaitableEffect<z.infer<T>[], SurrealDBError> {
|
|
541
|
-
const self = this
|
|
542
690
|
return toAwaitableEffect(
|
|
543
|
-
Effect.gen(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
691
|
+
Effect.gen(
|
|
692
|
+
function* (this: SurrealDBService) {
|
|
693
|
+
const boundQuery = yield* this.normalizeBoundQueryEffect(query)
|
|
694
|
+
const results = yield* this.query<unknown>(boundQuery)
|
|
695
|
+
return yield* Effect.forEach(results, (row) => this.parseSchemaEffect(schema, row))
|
|
696
|
+
}.bind(this),
|
|
697
|
+
),
|
|
548
698
|
)
|
|
549
699
|
}
|
|
550
700
|
|
|
551
701
|
private runSqlFile(file: Bun.BunFile): Effect.Effect<void, SurrealDBError> {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
702
|
+
return Effect.gen(
|
|
703
|
+
function* (this: SurrealDBService) {
|
|
704
|
+
const sql = (yield* Effect.tryPromise({
|
|
705
|
+
try: () => file.text(),
|
|
706
|
+
catch: (error) =>
|
|
707
|
+
new SurrealDBError({ message: `Failed to read schema file: ${getErrorMessage(error)}`, cause: error }),
|
|
708
|
+
})).trim()
|
|
709
|
+
if (!sql) {
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
yield* this.queryAll<unknown>(new BoundQuery(sql))
|
|
714
|
+
}.bind(this),
|
|
715
|
+
)
|
|
565
716
|
}
|
|
566
717
|
|
|
567
718
|
applySchema(schemaFiles: readonly Bun.BunFile[]): AwaitableEffect<void, SurrealDBError> {
|
|
@@ -575,27 +726,28 @@ export class SurrealDBService {
|
|
|
575
726
|
filter: Record<string, unknown>,
|
|
576
727
|
schema: T,
|
|
577
728
|
): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
|
|
578
|
-
const self = this
|
|
579
729
|
return toAwaitableEffect(
|
|
580
|
-
Effect.gen(
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
730
|
+
Effect.gen(
|
|
731
|
+
function* (this: SurrealDBService) {
|
|
732
|
+
const selection = yield* this.buildFilterExpressionEffect(filter)
|
|
733
|
+
const client = yield* this.ensureConnectedEffect(`SELECT * FROM ${table} LIMIT 1`)
|
|
734
|
+
let query = client.select<unknown>(new Table(table))
|
|
735
|
+
if (selection) {
|
|
736
|
+
query = query.where(selection)
|
|
737
|
+
}
|
|
587
738
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
739
|
+
const rows = yield* Effect.tryPromise({
|
|
740
|
+
try: () => query.limit(1),
|
|
741
|
+
catch: (error) =>
|
|
742
|
+
new SurrealDBError({
|
|
743
|
+
message: `Failed to find record in ${table}: ${getErrorMessage(error)}`,
|
|
744
|
+
cause: error,
|
|
745
|
+
}),
|
|
746
|
+
})
|
|
747
|
+
const first = rows.at(0)
|
|
748
|
+
return first ? yield* this.parseSchemaEffect(schema, first) : null
|
|
749
|
+
}.bind(this),
|
|
750
|
+
),
|
|
599
751
|
)
|
|
600
752
|
}
|
|
601
753
|
|
|
@@ -605,69 +757,70 @@ export class SurrealDBService {
|
|
|
605
757
|
schema: T,
|
|
606
758
|
options?: FindManyOptions,
|
|
607
759
|
): AwaitableEffect<z.infer<T>[], SurrealDBError> {
|
|
608
|
-
const self = this
|
|
609
760
|
return toAwaitableEffect(
|
|
610
|
-
Effect.gen(
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
761
|
+
Effect.gen(
|
|
762
|
+
function* (this: SurrealDBService) {
|
|
763
|
+
const filterKeys = Object.keys(filter)
|
|
764
|
+
const selection = yield* this.buildFilterExpressionEffect(filter)
|
|
765
|
+
const orderBy = options?.orderBy
|
|
766
|
+
|
|
767
|
+
if (orderBy !== undefined) {
|
|
768
|
+
yield* this.assertValidIdentifierEffect(orderBy, 'orderBy field')
|
|
769
|
+
yield* this.assertValidIdentifierEffect(table, 'table name')
|
|
770
|
+
for (const key of filterKeys) {
|
|
771
|
+
yield* this.assertValidIdentifierEffect(key, 'filter field')
|
|
772
|
+
}
|
|
773
|
+
const rawOrderDir: unknown = options?.orderDir
|
|
774
|
+
if (rawOrderDir !== undefined && rawOrderDir !== 'ASC' && rawOrderDir !== 'DESC') {
|
|
775
|
+
return yield* new SurrealDBError({
|
|
776
|
+
message: `Invalid orderDir value: ${describeInvalidValue(rawOrderDir)}`,
|
|
777
|
+
})
|
|
778
|
+
}
|
|
779
|
+
const orderDir = rawOrderDir ?? 'ASC'
|
|
780
|
+
const limit = options?.limit
|
|
781
|
+
const offset = options?.offset
|
|
782
|
+
const vars: Record<string, unknown> = yield* this.normalizeMutationDataEffect(filter)
|
|
783
|
+
let sql = `SELECT * FROM ${table}`
|
|
784
|
+
if (filterKeys.length > 0) {
|
|
785
|
+
const conditions = filterKeys.map((key) => `${key} = $${key}`).join(' AND ')
|
|
786
|
+
sql += ` WHERE ${conditions}`
|
|
787
|
+
}
|
|
788
|
+
sql += ` ORDER BY ${orderBy} ${orderDir}`
|
|
789
|
+
if (limit !== undefined) {
|
|
790
|
+
sql += ' LIMIT $limitParam'
|
|
791
|
+
vars.limitParam = limit
|
|
792
|
+
}
|
|
793
|
+
if (offset !== undefined) {
|
|
794
|
+
sql += ' START $offsetParam'
|
|
795
|
+
vars.offsetParam = offset
|
|
796
|
+
}
|
|
797
|
+
const rows = yield* this.query<unknown>(new BoundQuery(sql, vars))
|
|
798
|
+
return yield* Effect.forEach(rows, (row) => this.parseSchemaEffect(schema, row))
|
|
626
799
|
}
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (filterKeys.length > 0) {
|
|
633
|
-
const conditions = filterKeys.map((key) => `${key} = $${key}`).join(' AND ')
|
|
634
|
-
sql += ` WHERE ${conditions}`
|
|
800
|
+
|
|
801
|
+
const client = yield* this.ensureConnectedEffect(`SELECT * FROM ${table}`)
|
|
802
|
+
let query = client.select<unknown>(new Table(table))
|
|
803
|
+
if (selection) {
|
|
804
|
+
query = query.where(selection)
|
|
635
805
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
sql += ' LIMIT $limitParam'
|
|
639
|
-
vars.limitParam = limit
|
|
806
|
+
if (options?.offset !== undefined) {
|
|
807
|
+
query = query.start(options.offset)
|
|
640
808
|
}
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
vars.offsetParam = offset
|
|
809
|
+
if (options?.limit !== undefined) {
|
|
810
|
+
query = query.limit(options.limit)
|
|
644
811
|
}
|
|
645
|
-
const rows = yield* self.query<unknown>(new BoundQuery(sql, vars))
|
|
646
|
-
return rows.map((row) => self.parseSchema(schema, row))
|
|
647
|
-
}
|
|
648
812
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const rows = yield* Effect.tryPromise({
|
|
662
|
-
try: () => query,
|
|
663
|
-
catch: (error) =>
|
|
664
|
-
new SurrealDBError({
|
|
665
|
-
message: `Failed to run findMany query for ${table}: ${getErrorMessage(error)}`,
|
|
666
|
-
cause: error,
|
|
667
|
-
}),
|
|
668
|
-
})
|
|
669
|
-
return rows.map((row) => self.parseSchema(schema, row))
|
|
670
|
-
}),
|
|
813
|
+
const rows = yield* Effect.tryPromise({
|
|
814
|
+
try: () => query,
|
|
815
|
+
catch: (error) =>
|
|
816
|
+
new SurrealDBError({
|
|
817
|
+
message: `Failed to run findMany query for ${table}: ${getErrorMessage(error)}`,
|
|
818
|
+
cause: error,
|
|
819
|
+
}),
|
|
820
|
+
})
|
|
821
|
+
return yield* Effect.forEach(rows, (row) => this.parseSchemaEffect(schema, row))
|
|
822
|
+
}.bind(this),
|
|
823
|
+
),
|
|
671
824
|
)
|
|
672
825
|
}
|
|
673
826
|
|
|
@@ -676,31 +829,33 @@ export class SurrealDBService {
|
|
|
676
829
|
data: Record<string, unknown>,
|
|
677
830
|
schema: T,
|
|
678
831
|
): AwaitableEffect<z.infer<T>, SurrealDBError> {
|
|
679
|
-
const self = this
|
|
680
832
|
return toAwaitableEffect(
|
|
681
|
-
Effect.gen(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
833
|
+
Effect.gen(
|
|
834
|
+
function* (this: SurrealDBService) {
|
|
835
|
+
const keys = Object.keys(data)
|
|
836
|
+
if (keys.length === 0) {
|
|
837
|
+
return yield* new SurrealDBError({ message: `Cannot create record in ${table} with empty data` })
|
|
838
|
+
}
|
|
686
839
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
840
|
+
const client = yield* this.ensureConnectedEffect(`CREATE ${table}`)
|
|
841
|
+
const normalizedData = yield* this.normalizeMutationDataEffect(data)
|
|
842
|
+
const created = yield* Effect.tryPromise({
|
|
843
|
+
try: () => client.create<unknown>(new Table(table)).content(normalizedData).output('after'),
|
|
844
|
+
catch: (error) =>
|
|
845
|
+
new SurrealDBError({
|
|
846
|
+
message: `Failed to create record in ${table}: ${getErrorMessage(error)}`,
|
|
847
|
+
cause: error,
|
|
848
|
+
}),
|
|
849
|
+
})
|
|
850
|
+
const first = Array.isArray(created) ? created.at(0) : created
|
|
697
851
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
852
|
+
if (!first) {
|
|
853
|
+
return yield* new SurrealDBError({ message: `Failed to create record in ${table}` })
|
|
854
|
+
}
|
|
701
855
|
|
|
702
|
-
|
|
703
|
-
|
|
856
|
+
return yield* this.parseSchemaEffect(schema, first)
|
|
857
|
+
}.bind(this),
|
|
858
|
+
),
|
|
704
859
|
)
|
|
705
860
|
}
|
|
706
861
|
|
|
@@ -710,26 +865,28 @@ export class SurrealDBService {
|
|
|
710
865
|
data: Record<string, unknown>,
|
|
711
866
|
schema: T,
|
|
712
867
|
): AwaitableEffect<z.infer<T>, SurrealDBError> {
|
|
713
|
-
const self = this
|
|
714
868
|
return toAwaitableEffect(
|
|
715
|
-
Effect.gen(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
869
|
+
Effect.gen(
|
|
870
|
+
function* (this: SurrealDBService) {
|
|
871
|
+
const recordId = yield* this.normalizeRecordIdEffect(id, table)
|
|
872
|
+
const keys = Object.keys(data)
|
|
873
|
+
if (keys.length === 0) {
|
|
874
|
+
return yield* new SurrealDBError({ message: `Cannot create record in ${table} with empty data` })
|
|
875
|
+
}
|
|
721
876
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
877
|
+
const client = yield* this.ensureConnectedEffect(`CREATE ${recordId.toString()}`)
|
|
878
|
+
const normalizedData = yield* this.normalizeMutationDataEffect(data)
|
|
879
|
+
const created = yield* Effect.tryPromise({
|
|
880
|
+
try: () => client.create<unknown>(recordId).content(normalizedData).output('after'),
|
|
881
|
+
catch: (error) =>
|
|
882
|
+
new SurrealDBError({
|
|
883
|
+
message: `Failed to create record in ${table}: ${getErrorMessage(error)}`,
|
|
884
|
+
cause: error,
|
|
885
|
+
}),
|
|
886
|
+
})
|
|
887
|
+
return yield* this.parseSchemaEffect(schema, created)
|
|
888
|
+
}.bind(this),
|
|
889
|
+
),
|
|
733
890
|
)
|
|
734
891
|
}
|
|
735
892
|
|
|
@@ -740,28 +897,30 @@ export class SurrealDBService {
|
|
|
740
897
|
schema: T,
|
|
741
898
|
options?: { mutation?: RecordMutation },
|
|
742
899
|
): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
|
|
743
|
-
const self = this
|
|
744
900
|
return toAwaitableEffect(
|
|
745
|
-
Effect.gen(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
901
|
+
Effect.gen(
|
|
902
|
+
function* (this: SurrealDBService) {
|
|
903
|
+
const recordId = yield* this.normalizeRecordIdEffect(id, table)
|
|
904
|
+
const keys = Object.keys(data)
|
|
905
|
+
if (keys.length === 0) {
|
|
906
|
+
return yield* new SurrealDBError({ message: 'Cannot update record with empty data' })
|
|
907
|
+
}
|
|
751
908
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
909
|
+
const mutation = options?.mutation ?? 'merge'
|
|
910
|
+
const client = yield* this.ensureConnectedEffect(`UPDATE ${recordId.toString()}`)
|
|
911
|
+
const builder = client.update<unknown>(recordId)
|
|
912
|
+
const normalizedData = yield* this.normalizeMutationDataEffect(data)
|
|
913
|
+
const updated: Record<string, unknown> | null | undefined = yield* Effect.tryPromise({
|
|
914
|
+
try: () => configureMutation(builder, mutation, normalizedData).output('after'),
|
|
915
|
+
catch: (error) =>
|
|
916
|
+
new SurrealDBError({
|
|
917
|
+
message: `Failed to update record in ${table}: ${getErrorMessage(error)}`,
|
|
918
|
+
cause: error,
|
|
919
|
+
}),
|
|
920
|
+
})
|
|
921
|
+
return yield* this.parseOptionalSchemaEffect(schema, updated)
|
|
922
|
+
}.bind(this),
|
|
923
|
+
),
|
|
765
924
|
)
|
|
766
925
|
}
|
|
767
926
|
|
|
@@ -772,82 +931,89 @@ export class SurrealDBService {
|
|
|
772
931
|
schema: T,
|
|
773
932
|
options?: { mutation?: RecordMutation },
|
|
774
933
|
): AwaitableEffect<z.infer<T>, SurrealDBError> {
|
|
775
|
-
const self = this
|
|
776
934
|
return toAwaitableEffect(
|
|
777
|
-
Effect.gen(
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
935
|
+
Effect.gen(
|
|
936
|
+
function* (this: SurrealDBService) {
|
|
937
|
+
const recordId = yield* this.normalizeRecordIdEffect(id, table)
|
|
938
|
+
const keys = Object.keys(data)
|
|
939
|
+
if (keys.length === 0) {
|
|
940
|
+
return yield* new SurrealDBError({ message: 'Cannot upsert record with empty data' })
|
|
941
|
+
}
|
|
783
942
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
943
|
+
const mutation = options?.mutation ?? 'merge'
|
|
944
|
+
const client = yield* this.ensureConnectedEffect(`UPSERT ${recordId.toString()}`)
|
|
945
|
+
const builder = client.upsert<unknown>(recordId)
|
|
946
|
+
const normalizedData = yield* this.normalizeMutationDataEffect(data)
|
|
947
|
+
const upserted: Record<string, unknown> | null | undefined = yield* Effect.tryPromise({
|
|
948
|
+
try: () => configureMutation(builder, mutation, normalizedData).output('after'),
|
|
949
|
+
catch: (error) =>
|
|
950
|
+
new SurrealDBError({
|
|
951
|
+
message: `Failed to upsert record in ${table}: ${getErrorMessage(error)}`,
|
|
952
|
+
cause: error,
|
|
953
|
+
}),
|
|
954
|
+
})
|
|
955
|
+
const parsed = yield* this.parseOptionalSchemaEffect(schema, upserted)
|
|
956
|
+
if (parsed === null) {
|
|
957
|
+
return yield* new SurrealDBError({ message: `Failed to upsert record in ${table}` })
|
|
958
|
+
}
|
|
959
|
+
return parsed
|
|
960
|
+
}.bind(this),
|
|
961
|
+
),
|
|
801
962
|
)
|
|
802
963
|
}
|
|
803
964
|
|
|
804
965
|
deleteById(table: DatabaseTable, id: unknown): AwaitableEffect<boolean, SurrealDBError> {
|
|
805
|
-
const self = this
|
|
806
966
|
return toAwaitableEffect(
|
|
807
|
-
Effect.gen(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
967
|
+
Effect.gen(
|
|
968
|
+
function* (this: SurrealDBService) {
|
|
969
|
+
const recordId = yield* this.normalizeRecordIdEffect(id, table)
|
|
970
|
+
return yield* this.query<unknown>(new BoundQuery(`DELETE $recordId RETURN BEFORE`, { recordId })).pipe(
|
|
971
|
+
Effect.map((result) => result.length > 0),
|
|
972
|
+
Effect.catchTag('SurrealDBError', (error) => {
|
|
973
|
+
if (error.message.includes('does not exist')) {
|
|
974
|
+
return Effect.succeed(false)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return Effect.fail(this.toSurrealError(error, `DELETE ${recordId.toString()}`))
|
|
978
|
+
}),
|
|
979
|
+
)
|
|
980
|
+
}.bind(this),
|
|
981
|
+
),
|
|
820
982
|
)
|
|
821
983
|
}
|
|
822
984
|
|
|
823
985
|
deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): AwaitableEffect<number, SurrealDBError> {
|
|
824
|
-
const self = this
|
|
825
986
|
return toAwaitableEffect(
|
|
826
|
-
Effect.gen(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
987
|
+
Effect.gen(
|
|
988
|
+
function* (this: SurrealDBService) {
|
|
989
|
+
yield* this.assertValidIdentifierEffect(table, 'table name')
|
|
990
|
+
const filterKeys = Object.keys(filter)
|
|
991
|
+
if (filterKeys.length === 0) {
|
|
992
|
+
return yield* new SurrealDBError({ message: `Refusing to delete all records in ${table} without a filter` })
|
|
993
|
+
}
|
|
994
|
+
const { clause, bindings } = yield* this.buildBoundFilterClausesEffect(filter)
|
|
995
|
+
return yield* this.withTransaction((tx) =>
|
|
996
|
+
Effect.gen(
|
|
997
|
+
function* (this: SurrealDBService) {
|
|
998
|
+
const matchedRows = (yield* tx.query(
|
|
999
|
+
new BoundQuery(`SELECT id FROM ${table} WHERE ${clause}`, bindings),
|
|
1000
|
+
)) as Array<{ id: unknown }>
|
|
1001
|
+
|
|
1002
|
+
if (matchedRows.length === 0) {
|
|
1003
|
+
return 0
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
for (const row of matchedRows) {
|
|
1007
|
+
const recordId = yield* this.normalizeRecordIdEffect(row.id, table)
|
|
1008
|
+
yield* tx.delete(recordId)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return matchedRows.length
|
|
1012
|
+
}.bind(this),
|
|
1013
|
+
),
|
|
1014
|
+
)
|
|
1015
|
+
}.bind(this),
|
|
1016
|
+
),
|
|
851
1017
|
)
|
|
852
1018
|
}
|
|
853
1019
|
|
|
@@ -856,33 +1022,32 @@ export class SurrealDBService {
|
|
|
856
1022
|
where: ExprLike,
|
|
857
1023
|
data: Record<string, unknown>,
|
|
858
1024
|
): AwaitableEffect<number, SurrealDBError> {
|
|
859
|
-
const self = this
|
|
860
1025
|
return toAwaitableEffect(
|
|
861
|
-
Effect.gen(
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1026
|
+
Effect.gen(
|
|
1027
|
+
function* (this: SurrealDBService) {
|
|
1028
|
+
if (!where) {
|
|
1029
|
+
return yield* new SurrealDBError({
|
|
1030
|
+
message: `Refusing to update records in ${table} without a where clause`,
|
|
1031
|
+
})
|
|
1032
|
+
}
|
|
1033
|
+
const keys = Object.keys(data)
|
|
1034
|
+
if (keys.length === 0) {
|
|
1035
|
+
return yield* new SurrealDBError({ message: `Cannot update records in ${table} with empty data` })
|
|
1036
|
+
}
|
|
869
1037
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
client
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
})
|
|
884
|
-
return Array.isArray(updated) ? updated.length : 1
|
|
885
|
-
}),
|
|
1038
|
+
const client = yield* this.ensureConnectedEffect(`UPDATE ${table} WHERE ...`)
|
|
1039
|
+
const normalizedData = yield* this.normalizeMutationDataEffect(data)
|
|
1040
|
+
const updated = yield* Effect.tryPromise({
|
|
1041
|
+
try: () => client.update<unknown>(new Table(table)).where(where).merge(normalizedData).output('after'),
|
|
1042
|
+
catch: (error) =>
|
|
1043
|
+
new SurrealDBError({
|
|
1044
|
+
message: `Failed to update records in ${table}: ${getErrorMessage(error)}`,
|
|
1045
|
+
cause: error,
|
|
1046
|
+
}),
|
|
1047
|
+
})
|
|
1048
|
+
return Array.isArray(updated) ? updated.length : 1
|
|
1049
|
+
}.bind(this),
|
|
1050
|
+
),
|
|
886
1051
|
)
|
|
887
1052
|
}
|
|
888
1053
|
|
|
@@ -890,23 +1055,24 @@ export class SurrealDBService {
|
|
|
890
1055
|
table: DatabaseTable,
|
|
891
1056
|
data: Values<T> | Array<Values<T>>,
|
|
892
1057
|
): AwaitableEffect<T[], SurrealDBError> {
|
|
893
|
-
const self = this
|
|
894
1058
|
return toAwaitableEffect(
|
|
895
|
-
Effect.gen(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1059
|
+
Effect.gen(
|
|
1060
|
+
function* (this: SurrealDBService) {
|
|
1061
|
+
const client = yield* this.ensureConnectedEffect(`INSERT ${table}`)
|
|
1062
|
+
const normalized = Array.isArray(data)
|
|
1063
|
+
? yield* Effect.forEach(data, (item) => this.normalizeMutationDataEffect(item as Record<string, unknown>))
|
|
1064
|
+
: yield* this.normalizeMutationDataEffect(data as Record<string, unknown>)
|
|
1065
|
+
const inserted = yield* Effect.tryPromise({
|
|
1066
|
+
try: () => client.insert<T>(new Table(table), normalized as Values<T> | Array<Values<T>>).output('after'),
|
|
1067
|
+
catch: (error) =>
|
|
1068
|
+
new SurrealDBError({
|
|
1069
|
+
message: `Failed to insert rows into ${table}: ${getErrorMessage(error)}`,
|
|
1070
|
+
cause: error,
|
|
1071
|
+
}),
|
|
1072
|
+
})
|
|
1073
|
+
return inserted as T[]
|
|
1074
|
+
}.bind(this),
|
|
1075
|
+
),
|
|
910
1076
|
)
|
|
911
1077
|
}
|
|
912
1078
|
|
|
@@ -916,72 +1082,74 @@ export class SurrealDBService {
|
|
|
916
1082
|
to: RecordIdInput,
|
|
917
1083
|
data?: Values<T>,
|
|
918
1084
|
): AwaitableEffect<T | null, SurrealDBError> {
|
|
919
|
-
const fromRef = ensureRecordId(from)
|
|
920
|
-
const toRef = ensureRecordId(to)
|
|
921
|
-
|
|
922
|
-
const self = this
|
|
923
1085
|
return toAwaitableEffect(
|
|
924
|
-
Effect.gen(
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1086
|
+
Effect.gen(
|
|
1087
|
+
function* (this: SurrealDBService) {
|
|
1088
|
+
const fromRef = yield* ensureRecordIdEffect(from).pipe(Effect.mapError((error) => this.toSurrealError(error)))
|
|
1089
|
+
const toRef = yield* ensureRecordIdEffect(to).pipe(Effect.mapError((error) => this.toSurrealError(error)))
|
|
1090
|
+
const client = yield* this.ensureConnectedEffect(
|
|
1091
|
+
`RELATE ${fromRef.toString()}->${edgeTable}->${toRef.toString()}`,
|
|
1092
|
+
)
|
|
1093
|
+
const normalizedData = data
|
|
1094
|
+
? ((yield* this.normalizeMutationDataEffect(data as Record<string, unknown>)) as Values<T>)
|
|
1095
|
+
: undefined
|
|
1096
|
+
const related = (yield* Effect.tryPromise({
|
|
1097
|
+
try: () => client.relate<T>(fromRef, new Table(edgeTable), toRef, normalizedData).output('after'),
|
|
1098
|
+
catch: (error) =>
|
|
1099
|
+
new SurrealDBError({
|
|
1100
|
+
message: `Failed to relate records ${fromRef.toString()} -> ${edgeTable} -> ${toRef.toString()}: ${getErrorMessage(error)}`,
|
|
1101
|
+
cause: error,
|
|
1102
|
+
}),
|
|
1103
|
+
})) as T | T[] | null
|
|
1104
|
+
if (related === null) {
|
|
1105
|
+
return null
|
|
1106
|
+
}
|
|
1107
|
+
if (Array.isArray(related)) {
|
|
1108
|
+
return related.at(0) ?? null
|
|
1109
|
+
}
|
|
1110
|
+
return related
|
|
1111
|
+
}.bind(this),
|
|
1112
|
+
),
|
|
947
1113
|
)
|
|
948
1114
|
}
|
|
949
1115
|
|
|
950
1116
|
beginTransaction(): AwaitableEffect<DatabaseTransaction, SurrealDBError> {
|
|
951
|
-
const self = this
|
|
952
1117
|
return toAwaitableEffect(
|
|
953
|
-
Effect.gen(
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1118
|
+
Effect.gen(
|
|
1119
|
+
function* (this: SurrealDBService) {
|
|
1120
|
+
const client = yield* this.ensureConnectedEffect('BEGIN TRANSACTION')
|
|
1121
|
+
const tx = yield* Effect.tryPromise({
|
|
1122
|
+
try: () => client.beginTransaction(),
|
|
1123
|
+
catch: (error) =>
|
|
1124
|
+
new SurrealDBError({ message: `Failed to begin transaction: ${getErrorMessage(error)}`, cause: error }),
|
|
1125
|
+
})
|
|
1126
|
+
return this.wrapTransaction(tx)
|
|
1127
|
+
}.bind(this),
|
|
1128
|
+
),
|
|
962
1129
|
)
|
|
963
1130
|
}
|
|
964
1131
|
|
|
965
1132
|
withTransaction<T, E, R>(
|
|
966
1133
|
work: (tx: DatabaseTransaction) => Effect.Effect<T, E, R>,
|
|
967
1134
|
): Effect.Effect<T, SurrealDBError | E, R> {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1135
|
+
return Effect.gen(
|
|
1136
|
+
function* (this: SurrealDBService) {
|
|
1137
|
+
const tx = yield* this.beginTransaction()
|
|
1138
|
+
const cancelEffect = tx.cancel().pipe(
|
|
1139
|
+
Effect.catch((cancelError: SurrealDBError) =>
|
|
1140
|
+
Effect.sync(() => {
|
|
1141
|
+
this.logger?.warn?.(`Failed to cancel transaction after error: ${cancelError.message}`)
|
|
1142
|
+
}),
|
|
1143
|
+
),
|
|
1144
|
+
Effect.asVoid,
|
|
1145
|
+
)
|
|
979
1146
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1147
|
+
return yield* Effect.gen(function* () {
|
|
1148
|
+
const result = yield* work(tx)
|
|
1149
|
+
yield* tx.commit()
|
|
1150
|
+
return result
|
|
1151
|
+
}).pipe(Effect.onError(() => cancelEffect))
|
|
1152
|
+
}.bind(this),
|
|
1153
|
+
)
|
|
986
1154
|
}
|
|
987
1155
|
}
|