@lota-sdk/core 0.4.8 → 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 +11 -12
- package/src/ai/embedding-cache.ts +96 -22
- package/src/ai-gateway/ai-gateway.ts +766 -223
- package/src/config/agent-defaults.ts +189 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/background-processing.ts +1 -1
- package/src/config/constants.ts +8 -2
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +299 -19
- package/src/config/thread-defaults.ts +40 -20
- package/src/create-runtime.ts +200 -449
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +868 -601
- package/src/db/memory.ts +396 -280
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +288 -0
- package/src/db/service.ts +912 -779
- package/src/db/startup.ts +153 -68
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +96 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +123 -0
- package/src/effect/index.ts +24 -0
- package/src/effect/layers.ts +238 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +46 -0
- package/src/effect/services.ts +61 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +128 -83
- package/src/index.ts +48 -1
- package/src/openrouter/direct-provider.ts +11 -35
- package/src/queues/autonomous-job.queue.ts +117 -73
- package/src/queues/context-compaction.queue.ts +50 -17
- package/src/queues/delayed-node-promotion.queue.ts +46 -17
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +26 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
- package/src/queues/plan-scheduler.queue.ts +97 -33
- package/src/queues/post-chat-memory.queue.ts +56 -26
- package/src/queues/queue-factory.ts +227 -59
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +45 -11
- package/src/redis/connection.ts +182 -113
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +20 -0
- package/src/redis/stream-context.ts +92 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +5 -2
- package/src/runtime/agent-stream-helpers.ts +24 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +16 -4
- package/src/runtime/helper-model.ts +139 -48
- package/src/runtime/index.ts +7 -8
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/memory/memory-scope.ts +53 -0
- package/src/runtime/plugin-resolution.ts +124 -25
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +177 -130
- package/src/runtime/retrieval-adapters.ts +40 -6
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +150 -61
- package/src/runtime/runtime-extensions.ts +23 -25
- 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 +159 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
- package/src/runtime/social-chat/social-chat.ts +630 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +183 -111
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +253 -149
- package/src/services/artifact.service.ts +231 -149
- package/src/services/attachment.service.ts +171 -115
- package/src/services/autonomous-job.service.ts +890 -491
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +13 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +151 -88
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +278 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +101 -168
- package/src/services/graph-full-routing.ts +193 -0
- package/src/services/index.ts +19 -21
- package/src/services/institutional-memory.service.ts +213 -125
- package/src/services/learned-skill.service.ts +368 -260
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +50 -0
- package/src/services/memory/memory-preseeded.ts +86 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
- package/src/services/memory/memory.service.ts +674 -0
- package/src/services/memory/rerank.service.ts +201 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +29 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +153 -77
- package/src/services/ownership-dispatcher.service.ts +456 -263
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +169 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +405 -0
- package/src/services/plan/plan-deadline.service.ts +533 -0
- package/src/services/plan/plan-event-delivery.service.ts +266 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +522 -0
- package/src/services/plan/plan-executor-helpers.ts +307 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1737 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +637 -0
- package/src/services/plan/plan-scheduler.service.ts +379 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +36 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +131 -0
- package/src/services/plugin-executor.service.ts +102 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +288 -231
- package/src/services/recent-activity-title.service.ts +73 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +190 -122
- package/src/services/system-executor.service.ts +96 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +385 -0
- package/src/services/thread/thread-listing.ts +199 -0
- package/src/services/thread/thread-memory-block.ts +130 -0
- package/src/services/thread/thread-message.service.ts +379 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
- package/src/services/thread/thread-turn-streaming.ts +403 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +376 -0
- package/src/services/thread/thread.service.ts +344 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +334 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +3 -3
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +3 -3
- package/src/system-agents/memory.agent.ts +3 -3
- package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
- package/src/system-agents/skill-extractor.agent.ts +3 -3
- package/src/system-agents/skill-manager.agent.ts +3 -3
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +3 -3
- package/src/tools/execution-plan.tool.ts +241 -171
- package/src/tools/fetch-webpage.tool.ts +29 -18
- package/src/tools/firecrawl-client.ts +26 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +57 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +33 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +125 -84
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +25 -22
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +111 -20
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +164 -52
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +74 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/config/search.ts +0 -3
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/agent-types.ts +0 -1
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/memory-scope.ts +0 -43
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -914
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/rerank.service.ts +0 -156
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
package/src/db/service.ts
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
StringRecordId,
|
|
6
|
-
Surreal,
|
|
7
|
-
Table,
|
|
8
|
-
and,
|
|
9
|
-
createRemoteEngines,
|
|
10
|
-
eq,
|
|
11
|
-
} from 'surrealdb'
|
|
12
|
-
import type { ExprLike, Mutation, SurrealTransaction, Values } from 'surrealdb'
|
|
1
|
+
import { Duration, Effect, Schedule, Semaphore } from 'effect'
|
|
2
|
+
import { BoundQuery, ServerError, Surreal, Table, createRemoteEngines } from 'surrealdb'
|
|
3
|
+
import type { ExprLike, SurrealTransaction, Values } from 'surrealdb'
|
|
4
|
+
import { ZodError } from 'zod'
|
|
13
5
|
import type { z } from 'zod'
|
|
14
6
|
|
|
15
7
|
import { serverLogger } from '../config/logger'
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
8
|
+
import type { AwaitableEffect } from '../effect/awaitable-effect'
|
|
9
|
+
import { toAwaitableEffect } from '../effect/awaitable-effect'
|
|
10
|
+
import { getErrorMessage } from '../utils/errors'
|
|
11
|
+
import type { RecordIdInput, ensureRecordId } from './record-id'
|
|
12
|
+
import { ensureRecordIdEffect } from './record-id'
|
|
13
|
+
import {
|
|
14
|
+
assertValidIdentifier,
|
|
15
|
+
buildBoundFilterClauses,
|
|
16
|
+
buildFilterExpression,
|
|
17
|
+
configureMutation,
|
|
18
|
+
describeInvalidValue,
|
|
19
|
+
normalizeBoundQuery,
|
|
20
|
+
normalizeCreateTarget,
|
|
21
|
+
normalizeMutationData,
|
|
22
|
+
normalizeQueryRows,
|
|
23
|
+
normalizeRecordIdForTable,
|
|
24
|
+
normalizeSurrealValue,
|
|
25
|
+
normalizeTableValue,
|
|
26
|
+
normalizeTransactionQuery,
|
|
27
|
+
normalizeTransactionRecordId,
|
|
28
|
+
SurrealDBError,
|
|
29
|
+
} from './service-normalization'
|
|
30
|
+
import type { RecordMutation } from './service-normalization'
|
|
20
31
|
import type { DatabaseTable } from './tables'
|
|
21
|
-
|
|
22
|
-
export class SurrealDBError extends Error {
|
|
23
|
-
constructor(
|
|
24
|
-
message: string,
|
|
25
|
-
public readonly query?: string,
|
|
26
|
-
options?: ErrorOptions,
|
|
27
|
-
) {
|
|
28
|
-
super(message, options)
|
|
29
|
-
this.name = 'SurrealDBError'
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
+
import { isRetriableTransactionConflict } from './transaction-conflict'
|
|
32
33
|
|
|
33
34
|
export interface SurrealDatabaseConfig {
|
|
34
35
|
url: string
|
|
@@ -51,75 +52,97 @@ interface FindManyOptions {
|
|
|
51
52
|
orderDir?: 'ASC' | 'DESC'
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
type
|
|
55
|
+
type MutationBuilderSource = {
|
|
56
|
+
content: (data: Record<string, unknown>) => MutationBuilderSource
|
|
57
|
+
replace: (data: Record<string, unknown>) => MutationBuilderSource
|
|
58
|
+
merge: (data: Record<string, unknown>) => MutationBuilderSource
|
|
59
|
+
output: (mode: 'after' | 'before') => unknown
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type MutationBuilder = {
|
|
55
63
|
content: (data: Record<string, unknown>) => MutationBuilder
|
|
56
64
|
replace: (data: Record<string, unknown>) => MutationBuilder
|
|
57
65
|
merge: (data: Record<string, unknown>) => MutationBuilder
|
|
58
|
-
output: (mode: 'after' | 'before') =>
|
|
66
|
+
output: (mode: 'after' | 'before') => AwaitableEffect<unknown, SurrealDBError>
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
type
|
|
69
|
+
type CreateBuilderSource = {
|
|
70
|
+
content: (data: Record<string, unknown>) => CreateBuilderSource
|
|
71
|
+
output: (mode: 'after' | 'before') => unknown
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type PendingMutation = { mutation: RecordMutation; data: Record<string, unknown> }
|
|
62
75
|
|
|
63
76
|
export type CreateMutationBuilder = {
|
|
64
77
|
content: (data: Record<string, unknown>) => CreateMutationBuilder
|
|
65
|
-
output: (mode: 'after' | 'before') =>
|
|
78
|
+
output: (mode: 'after' | 'before') => AwaitableEffect<unknown, SurrealDBError>
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
export interface DatabaseTransaction {
|
|
69
|
-
query: (query: unknown) =>
|
|
82
|
+
query: (query: unknown) => AwaitableEffect<unknown, SurrealDBError>
|
|
70
83
|
create: (target: unknown) => CreateMutationBuilder
|
|
71
84
|
update: (target: unknown) => MutationBuilder
|
|
72
|
-
delete: (target: unknown) =>
|
|
73
|
-
relate: (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
data: Record<string, unknown>,
|
|
82
|
-
): MutationBuilder {
|
|
83
|
-
if (mutation === 'content') {
|
|
84
|
-
return builder.content(data)
|
|
85
|
-
}
|
|
86
|
-
if (mutation === 'replace') {
|
|
87
|
-
return builder.replace(data)
|
|
88
|
-
}
|
|
89
|
-
return builder.merge(data)
|
|
85
|
+
delete: (target: unknown) => AwaitableEffect<unknown, SurrealDBError>
|
|
86
|
+
relate: (
|
|
87
|
+
from: unknown,
|
|
88
|
+
edgeTable: unknown,
|
|
89
|
+
to: unknown,
|
|
90
|
+
data?: Values<Record<string, unknown>>,
|
|
91
|
+
) => AwaitableEffect<unknown, SurrealDBError>
|
|
92
|
+
commit: () => AwaitableEffect<void, SurrealDBError>
|
|
93
|
+
cancel: () => AwaitableEffect<void, SurrealDBError>
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
const CONNECT_MAX_ATTEMPTS = 5
|
|
93
97
|
const CONNECT_RETRY_BASE_DELAY_MS = 100
|
|
94
|
-
const CONNECT_RETRY_JITTER_MS = 50
|
|
95
98
|
const CONNECT_ATTEMPT_TIMEOUT_MS = 5_000
|
|
96
99
|
|
|
100
|
+
function isRetriableConnectError(error: unknown): boolean {
|
|
101
|
+
if (isRetriableTransactionConflict(error)) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
|
106
|
+
return /(timed out connecting to SurrealDB|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|network|socket|connection)/i.test(
|
|
107
|
+
message,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isSurrealDBError(error: unknown): error is SurrealDBError {
|
|
112
|
+
return typeof error === 'object' && error !== null && (error as { _tag?: unknown })._tag === 'SurrealDBError'
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
export class SurrealDBService {
|
|
98
116
|
private client: Surreal | null = null
|
|
99
117
|
private isConnected = false
|
|
100
|
-
|
|
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)
|
|
101
121
|
|
|
102
122
|
constructor(
|
|
103
123
|
private readonly config: SurrealDatabaseConfig,
|
|
104
124
|
private readonly logger?: SurrealDatabaseLogger,
|
|
105
125
|
) {}
|
|
106
126
|
|
|
107
|
-
private toSurrealError(error: unknown, query?: string):
|
|
108
|
-
if (error
|
|
109
|
-
|
|
127
|
+
private toSurrealError(error: unknown, query?: string): SurrealDBError {
|
|
128
|
+
if (isSurrealDBError(error)) {
|
|
129
|
+
return error
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (error instanceof ZodError) {
|
|
133
|
+
return new SurrealDBError({ message: error.message, query: query, cause: error })
|
|
110
134
|
}
|
|
111
135
|
|
|
112
136
|
if (error instanceof ServerError) {
|
|
113
|
-
|
|
137
|
+
return new SurrealDBError({ message: `${error.name}: ${error.message}`, query: query, cause: error })
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
if (error instanceof Error) {
|
|
117
|
-
|
|
141
|
+
return new SurrealDBError({ message: error.message, query: query, cause: error })
|
|
118
142
|
}
|
|
119
143
|
|
|
120
|
-
|
|
144
|
+
return new SurrealDBError({ message: String(error), query })
|
|
121
145
|
}
|
|
122
|
-
|
|
123
146
|
private isEmbeddedEngine(url: string) {
|
|
124
147
|
return (
|
|
125
148
|
url === 'mem://' ||
|
|
@@ -130,433 +153,471 @@ export class SurrealDBService {
|
|
|
130
153
|
)
|
|
131
154
|
}
|
|
132
155
|
|
|
133
|
-
private
|
|
156
|
+
private getOrCreateClient(): Effect.Effect<Surreal, SurrealDBError> {
|
|
134
157
|
if (this.client) {
|
|
135
|
-
return this.client
|
|
158
|
+
return Effect.succeed(this.client)
|
|
136
159
|
}
|
|
137
160
|
|
|
138
161
|
const codecOptions = { useNativeDates: true }
|
|
139
162
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
>['engines'],
|
|
146
|
-
codecOptions,
|
|
147
|
-
})
|
|
148
|
-
return this.client
|
|
149
|
-
}
|
|
163
|
+
return Effect.gen(
|
|
164
|
+
function* (this: SurrealDBService) {
|
|
165
|
+
if (this.client) {
|
|
166
|
+
return this.client
|
|
167
|
+
}
|
|
150
168
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
186
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
187
|
+
this.client = new Surreal({ engines: createRemoteEngines(), codecOptions })
|
|
188
|
+
return this.client
|
|
189
|
+
}.bind(this),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
159
192
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
private resetClient(): Effect.Effect<void, never> {
|
|
194
|
+
if (this.client === null) {
|
|
195
|
+
return Effect.void
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const client = this.client
|
|
199
|
+
|
|
200
|
+
return Effect.tryPromise({
|
|
201
|
+
try: () => client.close(),
|
|
202
|
+
catch: (error) =>
|
|
203
|
+
new SurrealDBError({ message: `Failed to close database client: ${getErrorMessage(error)}`, cause: error }),
|
|
204
|
+
}).pipe(
|
|
205
|
+
Effect.catch((error: SurrealDBError) =>
|
|
206
|
+
Effect.sync(() => {
|
|
207
|
+
serverLogger.warn`Failed to close database client: ${error.message}`
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
Effect.asVoid,
|
|
211
|
+
Effect.ensuring(
|
|
212
|
+
Effect.sync(() => {
|
|
213
|
+
this.client = null
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
216
|
+
)
|
|
167
217
|
}
|
|
168
218
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
219
|
+
connect(): AwaitableEffect<void, SurrealDBError> {
|
|
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
|
+
}
|
|
173
228
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
message.includes('resource busy') ||
|
|
181
|
-
message.includes('this transaction can be retried')
|
|
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),
|
|
234
|
+
),
|
|
182
235
|
)
|
|
183
236
|
}
|
|
184
237
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
await this.connectPromise
|
|
192
|
-
return
|
|
193
|
-
}
|
|
238
|
+
private connectLockedEffect(): Effect.Effect<void, SurrealDBError> {
|
|
239
|
+
return Effect.gen(
|
|
240
|
+
function* (this: SurrealDBService) {
|
|
241
|
+
if (this.isConnected) {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
194
244
|
|
|
195
|
-
|
|
196
|
-
|
|
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(() =>
|
|
269
|
+
Effect.sync(() => {
|
|
270
|
+
this.isConnected = true
|
|
271
|
+
this.logger?.info?.('Connected to SurrealDB')
|
|
272
|
+
}),
|
|
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
|
+
)
|
|
197
286
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
287
|
+
yield* connectEffect
|
|
288
|
+
}.bind(this),
|
|
289
|
+
)
|
|
290
|
+
}
|
|
201
291
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}),
|
|
210
|
-
CONNECT_ATTEMPT_TIMEOUT_MS,
|
|
211
|
-
`SurrealDB connect (${this.config.url})`,
|
|
212
|
-
)
|
|
292
|
+
disconnect(): AwaitableEffect<void, SurrealDBError> {
|
|
293
|
+
return toAwaitableEffect(
|
|
294
|
+
Effect.gen(
|
|
295
|
+
function* (this: SurrealDBService) {
|
|
296
|
+
if (!this.isConnected) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
213
299
|
|
|
214
|
-
this.isConnected = true
|
|
215
|
-
this.logger?.info?.('Connected to SurrealDB')
|
|
216
|
-
return
|
|
217
|
-
} catch (error) {
|
|
218
|
-
lastError = error
|
|
219
300
|
this.isConnected = false
|
|
220
|
-
await this.resetClient()
|
|
221
301
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
302
|
+
const client = this.client
|
|
303
|
+
if (!client) {
|
|
304
|
+
this.client = null
|
|
305
|
+
return
|
|
226
306
|
}
|
|
227
307
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
),
|
|
232
321
|
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return this.toSurrealError(lastError)
|
|
238
|
-
})()
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
await this.connectPromise
|
|
242
|
-
} finally {
|
|
243
|
-
this.connectPromise = null
|
|
244
|
-
}
|
|
322
|
+
}.bind(this),
|
|
323
|
+
),
|
|
324
|
+
)
|
|
245
325
|
}
|
|
246
326
|
|
|
247
|
-
|
|
248
|
-
if (this.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (!this.isConnected) {
|
|
260
|
-
return
|
|
327
|
+
private ensureConnectedEffect(query?: string): Effect.Effect<Surreal, SurrealDBError, never> {
|
|
328
|
+
if (this.client === null || !this.isConnected) {
|
|
329
|
+
return this.connect().pipe(
|
|
330
|
+
Effect.flatMap(() => this.getOrCreateClient()),
|
|
331
|
+
Effect.mapError((error) =>
|
|
332
|
+
isSurrealDBError(error)
|
|
333
|
+
? error
|
|
334
|
+
: new SurrealDBError({ message: 'Database not connected', query, cause: error }),
|
|
335
|
+
),
|
|
336
|
+
)
|
|
261
337
|
}
|
|
262
338
|
|
|
263
|
-
this.
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
await this.client?.close()
|
|
267
|
-
} finally {
|
|
268
|
-
this.client = null
|
|
269
|
-
}
|
|
339
|
+
return Effect.succeed(this.client)
|
|
270
340
|
}
|
|
271
341
|
|
|
272
|
-
private
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (this.client === null) {
|
|
278
|
-
throw new SurrealDBError('Database not connected')
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return this.client
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
private normalizeRecordId(id: unknown, table: DatabaseTable): ReturnType<typeof ensureRecordId> {
|
|
285
|
-
try {
|
|
286
|
-
const recordId = ensureRecordId(id as RecordIdInput, table)
|
|
287
|
-
const resolvedTable = String(recordId.table)
|
|
288
|
-
if (resolvedTable !== table) {
|
|
289
|
-
throw new SurrealDBError(`Record id table mismatch: expected "${table}" but got "${resolvedTable}"`)
|
|
290
|
-
}
|
|
291
|
-
return recordId
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (error instanceof SurrealDBError) {
|
|
294
|
-
throw error
|
|
295
|
-
}
|
|
296
|
-
if (error instanceof Error) {
|
|
297
|
-
throw new SurrealDBError(`Invalid record id for table "${table}": ${error.message}`, undefined, {
|
|
298
|
-
cause: error,
|
|
299
|
-
})
|
|
300
|
-
}
|
|
301
|
-
throw new SurrealDBError(`Invalid record id for table "${table}"`)
|
|
302
|
-
}
|
|
342
|
+
private normalizeRecordIdEffect(
|
|
343
|
+
id: unknown,
|
|
344
|
+
table: DatabaseTable,
|
|
345
|
+
): Effect.Effect<ReturnType<typeof ensureRecordId>, SurrealDBError> {
|
|
346
|
+
return normalizeRecordIdForTable(id, table)
|
|
303
347
|
}
|
|
304
348
|
|
|
305
349
|
private normalizeQueryRows(statement: unknown, schema?: z.ZodTypeAny): unknown[] {
|
|
306
|
-
|
|
307
|
-
return schema ? statement.map((row) => this.parseSchema(schema, row)) : (statement as unknown[])
|
|
308
|
-
}
|
|
309
|
-
if (statement === null || statement === undefined) {
|
|
310
|
-
return []
|
|
311
|
-
}
|
|
312
|
-
return schema ? [this.parseSchema(schema, statement)] : [statement]
|
|
350
|
+
return normalizeQueryRows(statement, schema, (schemaValue, value) => this.parseSchema(schemaValue, value))
|
|
313
351
|
}
|
|
314
352
|
|
|
315
|
-
private
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (Array.isArray(value)) {
|
|
326
|
-
return value.map((entry) => this.normalizeParseValue(entry))
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (isSurrealRecordIdValue(value)) {
|
|
330
|
-
return ensureRecordId(value as RecordIdInput)
|
|
331
|
-
}
|
|
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
|
+
}
|
|
332
362
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
363
|
+
private normalizeParseValue(value: unknown): unknown {
|
|
364
|
+
return normalizeSurrealValue(value)
|
|
365
|
+
}
|
|
336
366
|
|
|
337
|
-
|
|
367
|
+
private normalizeParseValueEffect(value: unknown): Effect.Effect<unknown, SurrealDBError> {
|
|
368
|
+
return Effect.try({ try: () => this.normalizeParseValue(value), catch: (error) => this.toSurrealError(error) })
|
|
338
369
|
}
|
|
339
370
|
|
|
340
371
|
private parseSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> {
|
|
341
372
|
return schema.parse(this.normalizeParseValue(value))
|
|
342
373
|
}
|
|
343
374
|
|
|
344
|
-
private
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const expressions = entries.map(([key, value]) => eq(key, this.normalizeRuntimeValue(value)))
|
|
351
|
-
if (expressions.length === 1) {
|
|
352
|
-
return expressions[0]
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return and(...expressions)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private assertValidIdentifier(name: string, context: string): void {
|
|
359
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(name)) {
|
|
360
|
-
throw new SurrealDBError(`Invalid ${context}: "${name}"`)
|
|
361
|
-
}
|
|
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) })
|
|
362
380
|
}
|
|
363
381
|
|
|
364
|
-
private
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
} {
|
|
368
|
-
const entries = Object.entries(filter)
|
|
369
|
-
if (entries.length === 0) {
|
|
370
|
-
throw new SurrealDBError('Expected a non-empty filter')
|
|
382
|
+
private parseOptionalSchema<TSchema extends z.ZodTypeAny>(schema: TSchema, value: unknown): z.infer<TSchema> | null {
|
|
383
|
+
if (value === null || value === undefined) {
|
|
384
|
+
return null
|
|
371
385
|
}
|
|
372
386
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
this.assertValidIdentifier(field, 'filter field')
|
|
387
|
+
return this.parseSchema(schema, value)
|
|
388
|
+
}
|
|
376
389
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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),
|
|
380
397
|
})
|
|
381
|
-
|
|
382
|
-
return { clause: clauses.join(' AND '), bindings }
|
|
383
398
|
}
|
|
384
399
|
|
|
385
|
-
private
|
|
386
|
-
return
|
|
400
|
+
private buildFilterExpression(filter: Record<string, unknown>): ExprLike | undefined {
|
|
401
|
+
return buildFilterExpression(filter)
|
|
387
402
|
}
|
|
388
403
|
|
|
389
|
-
private
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
value instanceof Date ||
|
|
396
|
-
value instanceof RecordId ||
|
|
397
|
-
value instanceof StringRecordId ||
|
|
398
|
-
value instanceof Table
|
|
399
|
-
) {
|
|
400
|
-
return value
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (Array.isArray(value)) {
|
|
404
|
-
return value.map((entry) => this.normalizeRuntimeValue(entry))
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (isSurrealRecordIdValue(value)) {
|
|
408
|
-
return ensureRecordId(value as RecordIdInput)
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (!isRecord(value)) {
|
|
412
|
-
return value
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if ('tb' in value && 'id' in value && Object.keys(value).length === 2) {
|
|
416
|
-
return ensureRecordId(value as unknown as RecordIdInput)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const entries = Object.entries(value)
|
|
420
|
-
if (entries.length === 0) {
|
|
421
|
-
return value
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return Object.fromEntries(entries.map(([key, entryValue]) => [key, this.normalizeRuntimeValue(entryValue)]))
|
|
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) })
|
|
425
408
|
}
|
|
426
409
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
return undefined
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return this.normalizeRuntimeValue(bindings) as Record<string, unknown>
|
|
410
|
+
private buildBoundFilterClausesEffect(
|
|
411
|
+
filter: Record<string, unknown>,
|
|
412
|
+
): Effect.Effect<{ clause: string; bindings: Record<string, unknown> }, SurrealDBError> {
|
|
413
|
+
return buildBoundFilterClauses(filter)
|
|
435
414
|
}
|
|
436
415
|
|
|
437
|
-
private
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
416
|
+
private assertValidIdentifierEffect(name: string, context: string): Effect.Effect<void, SurrealDBError> {
|
|
417
|
+
return assertValidIdentifier(name, context)
|
|
418
|
+
}
|
|
441
419
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
420
|
+
private normalizeBoundQueryEffect<T extends unknown[] = unknown[]>(
|
|
421
|
+
query: BoundQuery<T>,
|
|
422
|
+
): Effect.Effect<BoundQuery<T>, SurrealDBError> {
|
|
423
|
+
return normalizeBoundQuery(query)
|
|
424
|
+
}
|
|
445
425
|
|
|
446
|
-
|
|
426
|
+
private normalizeTransactionQueryEffect(query: unknown): Effect.Effect<BoundQuery, SurrealDBError> {
|
|
427
|
+
return normalizeTransactionQuery(query)
|
|
447
428
|
}
|
|
448
429
|
|
|
449
|
-
// Cast is safe: normalizeRuntimeValue preserves Record shape when input is a Record
|
|
450
430
|
private normalizeMutationData(data: Record<string, unknown>): Record<string, unknown> {
|
|
451
|
-
return
|
|
452
|
-
Object.entries(data)
|
|
453
|
-
.map(([key, value]) => [key, this.normalizeMutationFieldValue(value)] as const)
|
|
454
|
-
.filter((entry): entry is readonly [string, unknown] => entry[1] !== undefined),
|
|
455
|
-
) as Record<string, unknown>
|
|
431
|
+
return normalizeMutationData(data)
|
|
456
432
|
}
|
|
457
433
|
|
|
458
|
-
private
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (typeof value === 'string' && value.length > 0) {
|
|
464
|
-
return new Table(value)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
throw new SurrealDBError('Invalid table value')
|
|
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) })
|
|
468
438
|
}
|
|
469
439
|
|
|
470
|
-
private
|
|
471
|
-
|
|
472
|
-
return true
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (isSurrealRecordIdValue(value)) {
|
|
476
|
-
return true
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (typeof value === 'string') {
|
|
480
|
-
return /^[a-zA-Z][a-zA-Z0-9_]*:/.test(value)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (value && typeof value === 'object') {
|
|
484
|
-
const record = value as { tb?: unknown; id?: unknown }
|
|
485
|
-
return typeof record.tb === 'string' && record.id !== undefined && Object.keys(value).length === 2
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return false
|
|
440
|
+
private normalizeTableValueEffect(value: unknown): Effect.Effect<Table<string>, SurrealDBError> {
|
|
441
|
+
return normalizeTableValue(value)
|
|
489
442
|
}
|
|
490
443
|
|
|
491
|
-
private
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
444
|
+
private normalizeCreateTargetEffect(
|
|
445
|
+
value: unknown,
|
|
446
|
+
): Effect.Effect<Table<string> | ReturnType<typeof ensureRecordId>, SurrealDBError> {
|
|
447
|
+
return normalizeCreateTarget(value)
|
|
448
|
+
}
|
|
495
449
|
|
|
496
|
-
|
|
450
|
+
private normalizeTransactionRecordIdEffect(
|
|
451
|
+
value: unknown,
|
|
452
|
+
context: string,
|
|
453
|
+
): Effect.Effect<ReturnType<typeof ensureRecordId>, SurrealDBError> {
|
|
454
|
+
return normalizeTransactionRecordId(value, context)
|
|
497
455
|
}
|
|
498
456
|
|
|
499
|
-
private wrapMutationBuilder(
|
|
457
|
+
private wrapMutationBuilder(
|
|
458
|
+
builderEffect: Effect.Effect<MutationBuilderSource, SurrealDBError>,
|
|
459
|
+
pendingMutation?: PendingMutation,
|
|
460
|
+
): MutationBuilder {
|
|
500
461
|
return {
|
|
501
|
-
content: (data) => this.wrapMutationBuilder(
|
|
502
|
-
replace: (data) => this.wrapMutationBuilder(
|
|
503
|
-
merge: (data) => this.wrapMutationBuilder(
|
|
504
|
-
output:
|
|
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 }),
|
|
465
|
+
output: (mode) =>
|
|
466
|
+
toAwaitableEffect(
|
|
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
|
+
),
|
|
486
|
+
),
|
|
505
487
|
}
|
|
506
488
|
}
|
|
507
489
|
|
|
508
|
-
private wrapCreateBuilder(
|
|
490
|
+
private wrapCreateBuilder(
|
|
491
|
+
builderEffect: Effect.Effect<CreateBuilderSource, SurrealDBError>,
|
|
492
|
+
pendingData?: Record<string, unknown>,
|
|
493
|
+
): CreateMutationBuilder {
|
|
509
494
|
return {
|
|
510
|
-
content: (data) => this.wrapCreateBuilder(
|
|
511
|
-
output:
|
|
495
|
+
content: (data) => this.wrapCreateBuilder(builderEffect, data),
|
|
496
|
+
output: (mode) =>
|
|
497
|
+
toAwaitableEffect(
|
|
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
|
+
),
|
|
515
|
+
),
|
|
512
516
|
}
|
|
513
517
|
}
|
|
514
518
|
|
|
515
519
|
private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
|
|
516
520
|
return {
|
|
517
|
-
query:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
),
|
|
542
557
|
update: (target: unknown) =>
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
+
),
|
|
566
|
+
delete: (target: unknown) =>
|
|
567
|
+
toAwaitableEffect(
|
|
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),
|
|
556
581
|
),
|
|
557
582
|
),
|
|
558
|
-
|
|
559
|
-
|
|
583
|
+
relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
|
|
584
|
+
toAwaitableEffect(
|
|
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
|
+
),
|
|
604
|
+
),
|
|
605
|
+
commit: () =>
|
|
606
|
+
toAwaitableEffect(
|
|
607
|
+
Effect.tryPromise({
|
|
608
|
+
try: () => tx.commit(),
|
|
609
|
+
catch: (error) =>
|
|
610
|
+
new SurrealDBError({ message: `Failed to commit transaction: ${getErrorMessage(error)}`, cause: error }),
|
|
611
|
+
}),
|
|
612
|
+
),
|
|
613
|
+
cancel: () =>
|
|
614
|
+
toAwaitableEffect(
|
|
615
|
+
Effect.tryPromise({
|
|
616
|
+
try: () => tx.cancel(),
|
|
617
|
+
catch: (error) =>
|
|
618
|
+
new SurrealDBError({ message: `Failed to cancel transaction: ${getErrorMessage(error)}`, cause: error }),
|
|
619
|
+
}),
|
|
620
|
+
),
|
|
560
621
|
}
|
|
561
622
|
}
|
|
562
623
|
|
|
@@ -564,459 +625,531 @@ export class SurrealDBService {
|
|
|
564
625
|
return query.query
|
|
565
626
|
}
|
|
566
627
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
628
|
+
query<T>(query: BoundQuery): AwaitableEffect<T[], SurrealDBError> {
|
|
629
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
637
|
+
)
|
|
570
638
|
}
|
|
571
639
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
640
|
+
queryAll<T>(query: BoundQuery, schema?: z.ZodTypeAny): AwaitableEffect<T[][], SurrealDBError> {
|
|
641
|
+
return toAwaitableEffect(
|
|
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)}`,
|
|
652
|
+
query: queryText,
|
|
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
|
+
),
|
|
673
|
+
)
|
|
590
674
|
}
|
|
591
675
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
676
|
+
queryOne<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
|
|
677
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
686
|
+
)
|
|
596
687
|
}
|
|
597
688
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
689
|
+
queryMany<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): AwaitableEffect<z.infer<T>[], SurrealDBError> {
|
|
690
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
698
|
+
)
|
|
601
699
|
}
|
|
602
700
|
|
|
603
|
-
private
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
701
|
+
private runSqlFile(file: Bun.BunFile): Effect.Effect<void, SurrealDBError> {
|
|
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
|
+
}
|
|
608
712
|
|
|
609
|
-
|
|
713
|
+
yield* this.queryAll<unknown>(new BoundQuery(sql))
|
|
714
|
+
}.bind(this),
|
|
715
|
+
)
|
|
610
716
|
}
|
|
611
717
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
718
|
+
applySchema(schemaFiles: readonly Bun.BunFile[]): AwaitableEffect<void, SurrealDBError> {
|
|
719
|
+
return toAwaitableEffect(
|
|
720
|
+
Effect.forEach(schemaFiles, (schemaFile) => this.runSqlFile(schemaFile), { discard: true }),
|
|
721
|
+
)
|
|
616
722
|
}
|
|
617
723
|
|
|
618
|
-
|
|
724
|
+
findOne<T extends z.ZodTypeAny>(
|
|
619
725
|
table: DatabaseTable,
|
|
620
726
|
filter: Record<string, unknown>,
|
|
621
727
|
schema: T,
|
|
622
|
-
):
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
728
|
+
): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
|
|
729
|
+
return toAwaitableEffect(
|
|
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
|
+
}
|
|
738
|
+
|
|
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
|
+
),
|
|
751
|
+
)
|
|
638
752
|
}
|
|
639
753
|
|
|
640
|
-
|
|
754
|
+
findMany<T extends z.ZodTypeAny>(
|
|
641
755
|
table: DatabaseTable,
|
|
642
756
|
filter: Record<string, unknown>,
|
|
643
757
|
schema: T,
|
|
644
758
|
options?: FindManyOptions,
|
|
645
|
-
):
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (selection) {
|
|
687
|
-
query = query.where(selection)
|
|
688
|
-
}
|
|
689
|
-
if (options?.offset !== undefined) {
|
|
690
|
-
query = query.start(options.offset)
|
|
691
|
-
}
|
|
692
|
-
if (options?.limit !== undefined) {
|
|
693
|
-
query = query.limit(options.limit)
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const rows = await query
|
|
697
|
-
return rows.map((row) => this.parseSchema(schema, row))
|
|
698
|
-
} catch (error) {
|
|
699
|
-
return this.toSurrealError(error, `SELECT * FROM ${table}`)
|
|
700
|
-
}
|
|
701
|
-
}
|
|
759
|
+
): AwaitableEffect<z.infer<T>[], SurrealDBError> {
|
|
760
|
+
return toAwaitableEffect(
|
|
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))
|
|
799
|
+
}
|
|
702
800
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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)
|
|
805
|
+
}
|
|
806
|
+
if (options?.offset !== undefined) {
|
|
807
|
+
query = query.start(options.offset)
|
|
808
|
+
}
|
|
809
|
+
if (options?.limit !== undefined) {
|
|
810
|
+
query = query.limit(options.limit)
|
|
811
|
+
}
|
|
707
812
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
),
|
|
824
|
+
)
|
|
717
825
|
}
|
|
718
826
|
|
|
719
|
-
|
|
827
|
+
create<T extends z.ZodTypeAny>(
|
|
720
828
|
table: DatabaseTable,
|
|
721
829
|
data: Record<string, unknown>,
|
|
722
830
|
schema: T,
|
|
723
|
-
):
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
831
|
+
): AwaitableEffect<z.infer<T>, SurrealDBError> {
|
|
832
|
+
return toAwaitableEffect(
|
|
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
|
+
}
|
|
730
839
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
|
737
851
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
852
|
+
if (!first) {
|
|
853
|
+
return yield* new SurrealDBError({ message: `Failed to create record in ${table}` })
|
|
854
|
+
}
|
|
741
855
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
856
|
+
return yield* this.parseSchemaEffect(schema, first)
|
|
857
|
+
}.bind(this),
|
|
858
|
+
),
|
|
859
|
+
)
|
|
746
860
|
}
|
|
747
861
|
|
|
748
|
-
|
|
862
|
+
createWithId<T extends z.ZodTypeAny>(
|
|
749
863
|
table: DatabaseTable,
|
|
750
864
|
id: unknown,
|
|
751
865
|
data: Record<string, unknown>,
|
|
752
866
|
schema: T,
|
|
753
|
-
):
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
867
|
+
): AwaitableEffect<z.infer<T>, SurrealDBError> {
|
|
868
|
+
return toAwaitableEffect(
|
|
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
|
+
}
|
|
761
876
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
+
),
|
|
890
|
+
)
|
|
768
891
|
}
|
|
769
892
|
|
|
770
|
-
|
|
893
|
+
update<T extends z.ZodTypeAny>(
|
|
771
894
|
table: DatabaseTable,
|
|
772
895
|
id: unknown,
|
|
773
896
|
data: Record<string, unknown>,
|
|
774
897
|
schema: T,
|
|
775
898
|
options?: { mutation?: RecordMutation },
|
|
776
|
-
):
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
const mutation = options?.mutation ?? 'merge'
|
|
899
|
+
): AwaitableEffect<z.infer<T> | null, SurrealDBError> {
|
|
900
|
+
return toAwaitableEffect(
|
|
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
|
+
}
|
|
786
908
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
+
),
|
|
924
|
+
)
|
|
795
925
|
}
|
|
796
926
|
|
|
797
|
-
|
|
927
|
+
upsert<T extends z.ZodTypeAny>(
|
|
798
928
|
table: DatabaseTable,
|
|
799
929
|
id: unknown,
|
|
800
930
|
data: Record<string, unknown>,
|
|
801
931
|
schema: T,
|
|
802
932
|
options?: { mutation?: RecordMutation },
|
|
803
|
-
):
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
933
|
+
): AwaitableEffect<z.infer<T>, SurrealDBError> {
|
|
934
|
+
return toAwaitableEffect(
|
|
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
|
+
}
|
|
809
942
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
),
|
|
962
|
+
)
|
|
824
963
|
}
|
|
825
964
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
965
|
+
deleteById(table: DatabaseTable, id: unknown): AwaitableEffect<boolean, SurrealDBError> {
|
|
966
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
982
|
+
)
|
|
838
983
|
}
|
|
839
984
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
985
|
+
deleteWhere(table: DatabaseTable, filter: Record<string, unknown>): AwaitableEffect<number, SurrealDBError> {
|
|
986
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
1017
|
+
)
|
|
867
1018
|
}
|
|
868
1019
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1020
|
+
updateWhere(
|
|
1021
|
+
table: DatabaseTable,
|
|
1022
|
+
where: ExprLike,
|
|
1023
|
+
data: Record<string, unknown>,
|
|
1024
|
+
): AwaitableEffect<number, SurrealDBError> {
|
|
1025
|
+
return toAwaitableEffect(
|
|
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
|
+
}
|
|
877
1037
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
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
|
+
),
|
|
1051
|
+
)
|
|
893
1052
|
}
|
|
894
1053
|
|
|
895
|
-
|
|
1054
|
+
insert<T extends Record<string, unknown>>(
|
|
896
1055
|
table: DatabaseTable,
|
|
897
1056
|
data: Values<T> | Array<Values<T>>,
|
|
898
|
-
):
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1057
|
+
): AwaitableEffect<T[], SurrealDBError> {
|
|
1058
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
1076
|
+
)
|
|
912
1077
|
}
|
|
913
1078
|
|
|
914
|
-
|
|
1079
|
+
relate<T extends Record<string, unknown>>(
|
|
915
1080
|
from: RecordIdInput,
|
|
916
1081
|
edgeTable: DatabaseTable,
|
|
917
1082
|
to: RecordIdInput,
|
|
918
1083
|
data?: Values<T>,
|
|
919
|
-
):
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1084
|
+
): AwaitableEffect<T | null, SurrealDBError> {
|
|
1085
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
1113
|
+
)
|
|
942
1114
|
}
|
|
943
1115
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1116
|
+
beginTransaction(): AwaitableEffect<DatabaseTransaction, SurrealDBError> {
|
|
1117
|
+
return toAwaitableEffect(
|
|
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
|
+
),
|
|
1129
|
+
)
|
|
951
1130
|
}
|
|
952
1131
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}`,
|
|
1132
|
+
withTransaction<T, E, R>(
|
|
1133
|
+
work: (tx: DatabaseTransaction) => Effect.Effect<T, E, R>,
|
|
1134
|
+
): Effect.Effect<T, SurrealDBError | E, R> {
|
|
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,
|
|
967
1145
|
)
|
|
968
|
-
}
|
|
969
|
-
throw error
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
1146
|
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
)
|
|
981
|
-
return (...args) => Reflect.apply(value, target, args)
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function resolveConfiguredDatabaseService(): SurrealDBService {
|
|
985
|
-
if (!currentDatabaseService) {
|
|
986
|
-
throw new SurrealDBError('Database service not configured')
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
return currentDatabaseService
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
export const databaseService = new Proxy({} as SurrealDBService, {
|
|
993
|
-
get(_target, property) {
|
|
994
|
-
if (databaseServiceOverrides.has(property)) {
|
|
995
|
-
return databaseServiceOverrides.get(property)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const resolved = resolveConfiguredDatabaseService()
|
|
999
|
-
const value: unknown = Reflect.get(resolved, property)
|
|
1000
|
-
if (typeof value === 'function') {
|
|
1001
|
-
// SurrealDB SDK type gap — Reflect.get returns unknown, but we know the resolved target shape
|
|
1002
|
-
return bindTargetMethod(
|
|
1003
|
-
resolved as unknown as Record<PropertyKey, unknown>,
|
|
1004
|
-
value as (...args: unknown[]) => unknown,
|
|
1005
|
-
)
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
return value
|
|
1009
|
-
},
|
|
1010
|
-
set(_target, property, value) {
|
|
1011
|
-
databaseServiceOverrides.set(property, value)
|
|
1012
|
-
return true
|
|
1013
|
-
},
|
|
1014
|
-
})
|
|
1015
|
-
|
|
1016
|
-
export function setDatabaseService(db: SurrealDBService): void {
|
|
1017
|
-
if (db === databaseService) {
|
|
1018
|
-
return
|
|
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
|
+
)
|
|
1019
1154
|
}
|
|
1020
|
-
|
|
1021
|
-
currentDatabaseService = db
|
|
1022
1155
|
}
|