@lota-sdk/core 0.4.9 → 0.4.10

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