diff --git a/apps/server/src/persistence/AuthPairingLinks.ts b/apps/server/src/persistence/AuthPairingLinks.ts index add90f04803..c29b023d1d8 100644 --- a/apps/server/src/persistence/AuthPairingLinks.ts +++ b/apps/server/src/persistence/AuthPairingLinks.ts @@ -10,8 +10,8 @@ import { AuthEnvironmentScopes } from "@t3tools/contracts"; import { type AuthPairingLinkRepositoryError, - toPersistenceDecodeError, - toPersistenceSqlError, + PersistenceDecodeError, + PersistenceSqlError, } from "./Errors.ts"; export const AuthPairingLinkRecord = Schema.Struct({ @@ -90,8 +90,8 @@ export class AuthPairingLinkRepository extends Context.Service< function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): AuthPairingLinkRepositoryError => Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) + : new PersistenceSqlError({ operation: sqlOperation, cause }); } export const make = Effect.gen(function* () { diff --git a/apps/server/src/persistence/AuthSessions.ts b/apps/server/src/persistence/AuthSessions.ts index e3e8a19f5d0..17f76042d0a 100644 --- a/apps/server/src/persistence/AuthSessions.ts +++ b/apps/server/src/persistence/AuthSessions.ts @@ -15,8 +15,8 @@ import { import { type AuthSessionRepositoryError, - toPersistenceDecodeError, - toPersistenceSqlError, + PersistenceDecodeError, + PersistenceSqlError, } from "./Errors.ts"; export const AuthSessionClientMetadataRecord = Schema.Struct({ @@ -146,8 +146,8 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): AuthSessionRepositoryError => Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) + : new PersistenceSqlError({ operation: sqlOperation, cause }); } export const make = Effect.gen(function* () { diff --git a/apps/server/src/persistence/Errors.test.ts b/apps/server/src/persistence/Errors.test.ts new file mode 100644 index 00000000000..680a362e20a --- /dev/null +++ b/apps/server/src/persistence/Errors.test.ts @@ -0,0 +1,49 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +import { PersistenceDecodeError, PersistenceSqlError } from "./Errors.ts"; + +const decodeRuntimePayload = Schema.decodeUnknownEffect( + Schema.Struct({ + runtimePayload: Schema.Struct({ + attempt: Schema.Number, + }), + }), +); + +it("keeps SQL operation context without a tautological detail", () => { + const cause = new Error("database unavailable"); + const error = new PersistenceSqlError({ + operation: "AuthSessionRepository.list:query", + cause, + }); + + assert.equal(error.operation, "AuthSessionRepository.list:query"); + assert.equal(error.detail, undefined); + assert.equal(error.cause, cause); + assert.equal(error.message, "SQL error in AuthSessionRepository.list:query"); +}); + +it.effect("maps schema errors without copying rejected payloads into diagnostics", () => + Effect.gen(function* () { + const rejectedPayload = "runtime-payload-secret-sentinel"; + const cause = yield* Effect.flip( + decodeRuntimePayload({ + runtimePayload: { + attempt: rejectedPayload, + }, + }), + ); + const error = PersistenceDecodeError.fromSchemaError( + "ProviderSessionRuntimeRepository.list:decodeRows", + cause, + ); + + assert.equal(error.operation, "ProviderSessionRuntimeRepository.list:decodeRows"); + assert.equal(error.cause, cause); + assert.notInclude(error.issue, rejectedPayload); + assert.notInclude(error.message, rejectedPayload); + assert.include(error.issue, "InvalidType"); + }), +); diff --git a/apps/server/src/persistence/Errors.ts b/apps/server/src/persistence/Errors.ts index 2a3d7aff189..e7d081c8f72 100644 --- a/apps/server/src/persistence/Errors.ts +++ b/apps/server/src/persistence/Errors.ts @@ -1,6 +1,20 @@ import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; +function summarizeSchemaIssue(issue: SchemaIssue.Issue): string { + switch (issue._tag) { + case "Filter": + case "Encoding": + case "Pointer": + return `${issue._tag}(${summarizeSchemaIssue(issue.issue)})`; + case "Composite": + case "AnyOf": + return `${issue._tag}(${issue.issues.map(summarizeSchemaIssue).join(",")})`; + default: + return issue._tag; + } +} + // =============================== // Core Persistence Errors // =============================== @@ -9,12 +23,14 @@ export class PersistenceSqlError extends Schema.TaggedErrorClass new PersistenceSqlError({ @@ -42,22 +67,10 @@ export function toPersistenceSqlError(operation: string) { }); } +// Kept for orchestration/projection call sites, which are being revamped separately. export function toPersistenceDecodeError(operation: string) { - return (error: Schema.SchemaError): PersistenceDecodeError => - new PersistenceDecodeError({ - operation, - issue: SchemaIssue.makeFormatterDefault()(error.issue), - cause: error, - }); -} - -export function toPersistenceDecodeCauseError(operation: string) { - return (cause: unknown): PersistenceDecodeError => - new PersistenceDecodeError({ - operation, - issue: `Failed to execute ${operation}`, - cause, - }); + return (cause: Schema.SchemaError): PersistenceDecodeError => + PersistenceDecodeError.fromSchemaError(operation, cause); } export const isPersistenceError = (u: unknown) => diff --git a/apps/server/src/persistence/ProviderSessionRuntime.ts b/apps/server/src/persistence/ProviderSessionRuntime.ts index 6bbbfbd4e19..af48efdb50e 100644 --- a/apps/server/src/persistence/ProviderSessionRuntime.ts +++ b/apps/server/src/persistence/ProviderSessionRuntime.ts @@ -16,9 +16,9 @@ import { } from "@t3tools/contracts"; import { + PersistenceDecodeError, + PersistenceSqlError, type ProviderSessionRuntimeRepositoryError, - toPersistenceDecodeError, - toPersistenceSqlError, } from "./Errors.ts"; /** @@ -117,8 +117,8 @@ const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): ProviderSessionRuntimeRepositoryError => Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) + : new PersistenceSqlError({ operation: sqlOperation, cause }); } export const make = Effect.gen(function* () { @@ -235,9 +235,10 @@ export const make = Effect.gen(function* () { onNone: () => Effect.succeed(Option.none()), onSome: (row) => decodeRuntime(row).pipe( - Effect.mapError( - toPersistenceDecodeError( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", + cause, ), ), Effect.map((runtime) => Option.some(runtime)), @@ -259,8 +260,11 @@ export const make = Effect.gen(function* () { rows, (row) => decodeRuntime(row).pipe( - Effect.mapError( - toPersistenceDecodeError("ProviderSessionRuntimeRepository.list:rowToRuntime"), + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "ProviderSessionRuntimeRepository.list:rowToRuntime", + cause, + ), ), ), { concurrency: "unbounded" }, @@ -273,7 +277,11 @@ export const make = Effect.gen(function* () { ) => deleteRuntimeByThreadId(input).pipe( Effect.mapError( - toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteByThreadId:query"), + (cause) => + new PersistenceSqlError({ + operation: "ProviderSessionRuntimeRepository.deleteByThreadId:query", + cause, + }), ), );