| author | |
| committer | |
| log | 9c98cd60e866fb576e7029cd1152f75aca06452c |
| tree | e76fdea5e4abd9b8b89d5570aec6cd0edd46cd1f |
| parent | 48f88d522047198a660cacebbde854a617068720 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>5 files changed, 104 insertions(+), 34 deletions(-)
apps/server/src/persistence/AuthPairingLinks.ts+4-4| ... | @@ -10,8 +10,8 @@ import { AuthEnvironmentScopes } from "@t3tools/contracts"; | ... | @@ -10,8 +10,8 @@ import { AuthEnvironmentScopes } from "@t3tools/contracts"; |
| 10 | 10 | ||
| 11 | import { | 11 | import { |
| 12 | type AuthPairingLinkRepositoryError, | 12 | type AuthPairingLinkRepositoryError, |
| 13 | toPersistenceDecodeError, | 13 | PersistenceDecodeError, |
| 14 | toPersistenceSqlError, | 14 | PersistenceSqlError, |
| 15 | } from "./Errors.ts"; | 15 | } from "./Errors.ts"; |
| 16 | 16 | ||
| 17 | export const AuthPairingLinkRecord = Schema.Struct({ | 17 | export const AuthPairingLinkRecord = Schema.Struct({ |
| ... | @@ -90,8 +90,8 @@ export class AuthPairingLinkRepository extends Context.Service< | ... | @@ -90,8 +90,8 @@ export class AuthPairingLinkRepository extends Context.Service< |
| 90 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | 90 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 91 | return (cause: unknown): AuthPairingLinkRepositoryError => | 91 | return (cause: unknown): AuthPairingLinkRepositoryError => |
| 92 | Schema.isSchemaError(cause) | 92 | Schema.isSchemaError(cause) |
| 93 | ? toPersistenceDecodeError(decodeOperation)(cause) | 93 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) |
| 94 | : toPersistenceSqlError(sqlOperation)(cause); | 94 | : new PersistenceSqlError({ operation: sqlOperation, cause }); |
| 95 | } | 95 | } |
| 96 | 96 | ||
| 97 | export const make = Effect.gen(function* () { | 97 | export const make = Effect.gen(function* () { |
apps/server/src/persistence/AuthSessions.ts+4-4| ... | @@ -15,8 +15,8 @@ import { | ... | @@ -15,8 +15,8 @@ import { |
| 15 | 15 | ||
| 16 | import { | 16 | import { |
| 17 | type AuthSessionRepositoryError, | 17 | type AuthSessionRepositoryError, |
| 18 | toPersistenceDecodeError, | 18 | PersistenceDecodeError, |
| 19 | toPersistenceSqlError, | 19 | PersistenceSqlError, |
| 20 | } from "./Errors.ts"; | 20 | } from "./Errors.ts"; |
| 21 | 21 | ||
| 22 | export const AuthSessionClientMetadataRecord = Schema.Struct({ | 22 | export const AuthSessionClientMetadataRecord = Schema.Struct({ |
| ... | @@ -146,8 +146,8 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco | ... | @@ -146,8 +146,8 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco |
| 146 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | 146 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 147 | return (cause: unknown): AuthSessionRepositoryError => | 147 | return (cause: unknown): AuthSessionRepositoryError => |
| 148 | Schema.isSchemaError(cause) | 148 | Schema.isSchemaError(cause) |
| 149 | ? toPersistenceDecodeError(decodeOperation)(cause) | 149 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) |
| 150 | : toPersistenceSqlError(sqlOperation)(cause); | 150 | : new PersistenceSqlError({ operation: sqlOperation, cause }); |
| 151 | } | 151 | } |
| 152 | 152 | ||
| 153 | export const make = Effect.gen(function* () { | 153 | export const make = Effect.gen(function* () { |
apps/server/src/persistence/Errors.test.ts created+49| ... | @@ -0,0 +1,49 @@ | ||
| 1 | import { assert, it } from "@effect/vitest"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import * as Schema from "effect/Schema"; | ||
| 4 | |||
| 5 | import { PersistenceDecodeError, PersistenceSqlError } from "./Errors.ts"; | ||
| 6 | |||
| 7 | const decodeRuntimePayload = Schema.decodeUnknownEffect( | ||
| 8 | Schema.Struct({ | ||
| 9 | runtimePayload: Schema.Struct({ | ||
| 10 | attempt: Schema.Number, | ||
| 11 | }), | ||
| 12 | }), | ||
| 13 | ); | ||
| 14 | |||
| 15 | it("keeps SQL operation context without a tautological detail", () => { | ||
| 16 | const cause = new Error("database unavailable"); | ||
| 17 | const error = new PersistenceSqlError({ | ||
| 18 | operation: "AuthSessionRepository.list:query", | ||
| 19 | cause, | ||
| 20 | }); | ||
| 21 | |||
| 22 | assert.equal(error.operation, "AuthSessionRepository.list:query"); | ||
| 23 | assert.equal(error.detail, undefined); | ||
| 24 | assert.equal(error.cause, cause); | ||
| 25 | assert.equal(error.message, "SQL error in AuthSessionRepository.list:query"); | ||
| 26 | }); | ||
| 27 | |||
| 28 | it.effect("maps schema errors without copying rejected payloads into diagnostics", () => | ||
| 29 | Effect.gen(function* () { | ||
| 30 | const rejectedPayload = "runtime-payload-secret-sentinel"; | ||
| 31 | const cause = yield* Effect.flip( | ||
| 32 | decodeRuntimePayload({ | ||
| 33 | runtimePayload: { | ||
| 34 | attempt: rejectedPayload, | ||
| 35 | }, | ||
| 36 | }), | ||
| 37 | ); | ||
| 38 | const error = PersistenceDecodeError.fromSchemaError( | ||
| 39 | "ProviderSessionRuntimeRepository.list:decodeRows", | ||
| 40 | cause, | ||
| 41 | ); | ||
| 42 | |||
| 43 | assert.equal(error.operation, "ProviderSessionRuntimeRepository.list:decodeRows"); | ||
| 44 | assert.equal(error.cause, cause); | ||
| 45 | assert.notInclude(error.issue, rejectedPayload); | ||
| 46 | assert.notInclude(error.message, rejectedPayload); | ||
| 47 | assert.include(error.issue, "InvalidType"); | ||
| 48 | }), | ||
| 49 | ); | ||
apps/server/src/persistence/Errors.ts+30-17| ... | @@ -1,6 +1,20 @@ | ... | @@ -1,6 +1,20 @@ |
| 1 | import * as Schema from "effect/Schema"; | 1 | import * as Schema from "effect/Schema"; |
| 2 | import * as SchemaIssue from "effect/SchemaIssue"; | 2 | import * as SchemaIssue from "effect/SchemaIssue"; |
| 3 | 3 | ||
| 4 | function summarizeSchemaIssue(issue: SchemaIssue.Issue): string { | ||
| 5 | switch (issue._tag) { | ||
| 6 | case "Filter": | ||
| 7 | case "Encoding": | ||
| 8 | case "Pointer": | ||
| 9 | return `${issue._tag}(${summarizeSchemaIssue(issue.issue)})`; | ||
| 10 | case "Composite": | ||
| 11 | case "AnyOf": | ||
| 12 | return `${issue._tag}(${issue.issues.map(summarizeSchemaIssue).join(",")})`; | ||
| 13 | default: | ||
| 14 | return issue._tag; | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 4 | // =============================== | 18 | // =============================== |
| 5 | // Core Persistence Errors | 19 | // Core Persistence Errors |
| 6 | // =============================== | 20 | // =============================== |
| ... | @@ -9,12 +23,14 @@ export class PersistenceSqlError extends Schema.TaggedErrorClass<PersistenceSqlE | ... | @@ -9,12 +23,14 @@ export class PersistenceSqlError extends Schema.TaggedErrorClass<PersistenceSqlE |
| 9 | "PersistenceSqlError", | 23 | "PersistenceSqlError", |
| 10 | { | 24 | { |
| 11 | operation: Schema.String, | 25 | operation: Schema.String, |
| 12 | detail: Schema.String, | 26 | detail: Schema.optional(Schema.String), |
| 13 | cause: Schema.optional(Schema.Defect()), | 27 | cause: Schema.optional(Schema.Defect()), |
| 14 | }, | 28 | }, |
| 15 | ) { | 29 | ) { |
| 16 | override get message(): string { | 30 | override get message(): string { |
| 17 | return `SQL error in ${this.operation}: ${this.detail}`; | 31 | return this.detail === undefined |
| 32 | ? `SQL error in ${this.operation}` | ||
| 33 | : `SQL error in ${this.operation}: ${this.detail}`; | ||
| 18 | } | 34 | } |
| 19 | } | 35 | } |
| 20 | 36 | ||
| ... | @@ -26,6 +42,14 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD | ... | @@ -26,6 +42,14 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD |
| 26 | cause: Schema.optional(Schema.Defect()), | 42 | cause: Schema.optional(Schema.Defect()), |
| 27 | }, | 43 | }, |
| 28 | ) { | 44 | ) { |
| 45 | static fromSchemaError(operation: string, cause: Schema.SchemaError): PersistenceDecodeError { | ||
| 46 | return new PersistenceDecodeError({ | ||
| 47 | operation, | ||
| 48 | issue: summarizeSchemaIssue(cause.issue), | ||
| 49 | cause, | ||
| 50 | }); | ||
| 51 | } | ||
| 52 | |||
| 29 | override get message(): string { | 53 | override get message(): string { |
| 30 | return `Decode error in ${this.operation}: ${this.issue}`; | 54 | return `Decode error in ${this.operation}: ${this.issue}`; |
| 31 | } | 55 | } |
| ... | @@ -33,6 +57,7 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD | ... | @@ -33,6 +57,7 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD |
| 33 | const isPersistenceSqlError = Schema.is(PersistenceSqlError); | 57 | const isPersistenceSqlError = Schema.is(PersistenceSqlError); |
| 34 | const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); | 58 | const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); |
| 35 | 59 | ||
| 60 | // Kept for orchestration/projection call sites, which are being revamped separately. | ||
| 36 | export function toPersistenceSqlError(operation: string) { | 61 | export function toPersistenceSqlError(operation: string) { |
| 37 | return (cause: unknown): PersistenceSqlError => | 62 | return (cause: unknown): PersistenceSqlError => |
| 38 | new PersistenceSqlError({ | 63 | new PersistenceSqlError({ |
| ... | @@ -42,22 +67,10 @@ export function toPersistenceSqlError(operation: string) { | ... | @@ -42,22 +67,10 @@ export function toPersistenceSqlError(operation: string) { |
| 42 | }); | 67 | }); |
| 43 | } | 68 | } |
| 44 | 69 | ||
| 70 | // Kept for orchestration/projection call sites, which are being revamped separately. | ||
| 45 | export function toPersistenceDecodeError(operation: string) { | 71 | export function toPersistenceDecodeError(operation: string) { |
| 46 | return (error: Schema.SchemaError): PersistenceDecodeError => | 72 | return (cause: Schema.SchemaError): PersistenceDecodeError => |
| 47 | new PersistenceDecodeError({ | 73 | PersistenceDecodeError.fromSchemaError(operation, cause); |
| 48 | operation, | ||
| 49 | issue: SchemaIssue.makeFormatterDefault()(error.issue), | ||
| 50 | cause: error, | ||
| 51 | }); | ||
| 52 | } | ||
| 53 | |||
| 54 | export function toPersistenceDecodeCauseError(operation: string) { | ||
| 55 | return (cause: unknown): PersistenceDecodeError => | ||
| 56 | new PersistenceDecodeError({ | ||
| 57 | operation, | ||
| 58 | issue: `Failed to execute ${operation}`, | ||
| 59 | cause, | ||
| 60 | }); | ||
| 61 | } | 74 | } |
| 62 | 75 | ||
| 63 | export const isPersistenceError = (u: unknown) => | 76 | export const isPersistenceError = (u: unknown) => |
apps/server/src/persistence/ProviderSessionRuntime.ts+17-9| ... | @@ -16,9 +16,9 @@ import { | ... | @@ -16,9 +16,9 @@ import { |
| 16 | } from "@t3tools/contracts"; | 16 | } from "@t3tools/contracts"; |
| 17 | 17 | ||
| 18 | import { | 18 | import { |
| 19 | PersistenceDecodeError, | ||
| 20 | PersistenceSqlError, | ||
| 19 | type ProviderSessionRuntimeRepositoryError, | 21 | type ProviderSessionRuntimeRepositoryError, |
| 20 | toPersistenceDecodeError, | ||
| 21 | toPersistenceSqlError, | ||
| 22 | } from "./Errors.ts"; | 22 | } from "./Errors.ts"; |
| 23 | 23 | ||
| 24 | /** | 24 | /** |
| ... | @@ -117,8 +117,8 @@ const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; | ... | @@ -117,8 +117,8 @@ const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; |
| 117 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | 117 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 118 | return (cause: unknown): ProviderSessionRuntimeRepositoryError => | 118 | return (cause: unknown): ProviderSessionRuntimeRepositoryError => |
| 119 | Schema.isSchemaError(cause) | 119 | Schema.isSchemaError(cause) |
| 120 | ? toPersistenceDecodeError(decodeOperation)(cause) | 120 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) |
| 121 | : toPersistenceSqlError(sqlOperation)(cause); | 121 | : new PersistenceSqlError({ operation: sqlOperation, cause }); |
| 122 | } | 122 | } |
| 123 | 123 | ||
| 124 | export const make = Effect.gen(function* () { | 124 | export const make = Effect.gen(function* () { |
| ... | @@ -235,9 +235,10 @@ export const make = Effect.gen(function* () { | ... | @@ -235,9 +235,10 @@ export const make = Effect.gen(function* () { |
| 235 | onNone: () => Effect.succeed(Option.none()), | 235 | onNone: () => Effect.succeed(Option.none()), |
| 236 | onSome: (row) => | 236 | onSome: (row) => |
| 237 | decodeRuntime(row).pipe( | 237 | decodeRuntime(row).pipe( |
| 238 | Effect.mapError( | 238 | Effect.mapError((cause) => |
| 239 | toPersistenceDecodeError( | 239 | PersistenceDecodeError.fromSchemaError( |
| 240 | "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", | 240 | "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", |
| 241 | cause, | ||
| 241 | ), | 242 | ), |
| 242 | ), | 243 | ), |
| 243 | Effect.map((runtime) => Option.some(runtime)), | 244 | Effect.map((runtime) => Option.some(runtime)), |
| ... | @@ -259,8 +260,11 @@ export const make = Effect.gen(function* () { | ... | @@ -259,8 +260,11 @@ export const make = Effect.gen(function* () { |
| 259 | rows, | 260 | rows, |
| 260 | (row) => | 261 | (row) => |
| 261 | decodeRuntime(row).pipe( | 262 | decodeRuntime(row).pipe( |
| 262 | Effect.mapError( | 263 | Effect.mapError((cause) => |
| 263 | toPersistenceDecodeError("ProviderSessionRuntimeRepository.list:rowToRuntime"), | 264 | PersistenceDecodeError.fromSchemaError( |
| 265 | "ProviderSessionRuntimeRepository.list:rowToRuntime", | ||
| 266 | cause, | ||
| 267 | ), | ||
| 264 | ), | 268 | ), |
| 265 | ), | 269 | ), |
| 266 | { concurrency: "unbounded" }, | 270 | { concurrency: "unbounded" }, |
| ... | @@ -273,7 +277,11 @@ export const make = Effect.gen(function* () { | ... | @@ -273,7 +277,11 @@ export const make = Effect.gen(function* () { |
| 273 | ) => | 277 | ) => |
| 274 | deleteRuntimeByThreadId(input).pipe( | 278 | deleteRuntimeByThreadId(input).pipe( |
| 275 | Effect.mapError( | 279 | Effect.mapError( |
| 276 | toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteByThreadId:query"), | 280 | (cause) => |
| 281 | new PersistenceSqlError({ | ||
| 282 | operation: "ProviderSessionRuntimeRepository.deleteByThreadId:query", | ||
| 283 | cause, | ||
| 284 | }), | ||
| 277 | ), | 285 | ), |
| 278 | ); | 286 | ); |
| 279 | 287 |