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