| 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 | 10 | |
| 11 | 11 | import { |
| 12 | 12 | type AuthPairingLinkRepositoryError, |
| 13 | toPersistenceDecodeError, | |
| 14 | toPersistenceSqlError, | |
| 13 | PersistenceDecodeError, | |
| 14 | PersistenceSqlError, | |
| 15 | 15 | } from "./Errors.ts"; |
| 16 | 16 | |
| 17 | 17 | export const AuthPairingLinkRecord = Schema.Struct({ |
| ... | ... | @@ -90,8 +90,8 @@ export class AuthPairingLinkRepository extends Context.Service< |
| 90 | 90 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 91 | 91 | return (cause: unknown): AuthPairingLinkRepositoryError => |
| 92 | 92 | Schema.isSchemaError(cause) |
| 93 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 94 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 93 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) | |
| 94 | : new PersistenceSqlError({ operation: sqlOperation, cause }); | |
| 95 | 95 | } |
| 96 | 96 | |
| 97 | 97 | export const make = Effect.gen(function* () { |
apps/server/src/persistence/AuthSessions.ts+4-4| ... | ... | @@ -15,8 +15,8 @@ import { |
| 15 | 15 | |
| 16 | 16 | import { |
| 17 | 17 | type AuthSessionRepositoryError, |
| 18 | toPersistenceDecodeError, | |
| 19 | toPersistenceSqlError, | |
| 18 | PersistenceDecodeError, | |
| 19 | PersistenceSqlError, | |
| 20 | 20 | } from "./Errors.ts"; |
| 21 | 21 | |
| 22 | 22 | export const AuthSessionClientMetadataRecord = Schema.Struct({ |
| ... | ... | @@ -146,8 +146,8 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco |
| 146 | 146 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 147 | 147 | return (cause: unknown): AuthSessionRepositoryError => |
| 148 | 148 | Schema.isSchemaError(cause) |
| 149 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 150 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 149 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) | |
| 150 | : new PersistenceSqlError({ operation: sqlOperation, cause }); | |
| 151 | 151 | } |
| 152 | 152 | |
| 153 | 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 | 1 | import * as Schema from "effect/Schema"; |
| 2 | 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 | 19 | // Core Persistence Errors |
| 6 | 20 | // =============================== |
| ... | ... | @@ -9,12 +23,14 @@ export class PersistenceSqlError extends Schema.TaggedErrorClass<PersistenceSqlE |
| 9 | 23 | "PersistenceSqlError", |
| 10 | 24 | { |
| 11 | 25 | operation: Schema.String, |
| 12 | detail: Schema.String, | |
| 26 | detail: Schema.optional(Schema.String), | |
| 13 | 27 | cause: Schema.optional(Schema.Defect()), |
| 14 | 28 | }, |
| 15 | 29 | ) { |
| 16 | 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 | 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 | 53 | override get message(): string { |
| 30 | 54 | return `Decode error in ${this.operation}: ${this.issue}`; |
| 31 | 55 | } |
| ... | ... | @@ -33,6 +57,7 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD |
| 33 | 57 | const isPersistenceSqlError = Schema.is(PersistenceSqlError); |
| 34 | 58 | const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); |
| 35 | 59 | |
| 60 | // Kept for orchestration/projection call sites, which are being revamped separately. | |
| 36 | 61 | export function toPersistenceSqlError(operation: string) { |
| 37 | 62 | return (cause: unknown): PersistenceSqlError => |
| 38 | 63 | new PersistenceSqlError({ |
| ... | ... | @@ -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 | 71 | export function toPersistenceDecodeError(operation: string) { |
| 46 | return (error: Schema.SchemaError): PersistenceDecodeError => | |
| 47 | new PersistenceDecodeError({ | |
| 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 | }); | |
| 72 | return (cause: Schema.SchemaError): PersistenceDecodeError => | |
| 73 | PersistenceDecodeError.fromSchemaError(operation, cause); | |
| 61 | 74 | } |
| 62 | 75 | |
| 63 | 76 | export const isPersistenceError = (u: unknown) => |
apps/server/src/persistence/ProviderSessionRuntime.ts+17-9| ... | ... | @@ -16,9 +16,9 @@ import { |
| 16 | 16 | } from "@t3tools/contracts"; |
| 17 | 17 | |
| 18 | 18 | import { |
| 19 | PersistenceDecodeError, | |
| 20 | PersistenceSqlError, | |
| 19 | 21 | type ProviderSessionRuntimeRepositoryError, |
| 20 | toPersistenceDecodeError, | |
| 21 | toPersistenceSqlError, | |
| 22 | 22 | } from "./Errors.ts"; |
| 23 | 23 | |
| 24 | 24 | /** |
| ... | ... | @@ -117,8 +117,8 @@ const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; |
| 117 | 117 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { |
| 118 | 118 | return (cause: unknown): ProviderSessionRuntimeRepositoryError => |
| 119 | 119 | Schema.isSchemaError(cause) |
| 120 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 121 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 120 | ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) | |
| 121 | : new PersistenceSqlError({ operation: sqlOperation, cause }); | |
| 122 | 122 | } |
| 123 | 123 | |
| 124 | 124 | export const make = Effect.gen(function* () { |
| ... | ... | @@ -235,9 +235,10 @@ export const make = Effect.gen(function* () { |
| 235 | 235 | onNone: () => Effect.succeed(Option.none()), |
| 236 | 236 | onSome: (row) => |
| 237 | 237 | decodeRuntime(row).pipe( |
| 238 | Effect.mapError( | |
| 239 | toPersistenceDecodeError( | |
| 238 | Effect.mapError((cause) => | |
| 239 | PersistenceDecodeError.fromSchemaError( | |
| 240 | 240 | "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", |
| 241 | cause, | |
| 241 | 242 | ), |
| 242 | 243 | ), |
| 243 | 244 | Effect.map((runtime) => Option.some(runtime)), |
| ... | ... | @@ -259,8 +260,11 @@ export const make = Effect.gen(function* () { |
| 259 | 260 | rows, |
| 260 | 261 | (row) => |
| 261 | 262 | decodeRuntime(row).pipe( |
| 262 | Effect.mapError( | |
| 263 | toPersistenceDecodeError("ProviderSessionRuntimeRepository.list:rowToRuntime"), | |
| 263 | Effect.mapError((cause) => | |
| 264 | PersistenceDecodeError.fromSchemaError( | |
| 265 | "ProviderSessionRuntimeRepository.list:rowToRuntime", | |
| 266 | cause, | |
| 267 | ), | |
| 264 | 268 | ), |
| 265 | 269 | ), |
| 266 | 270 | { concurrency: "unbounded" }, |
| ... | ... | @@ -273,7 +277,11 @@ export const make = Effect.gen(function* () { |
| 273 | 277 | ) => |
| 274 | 278 | deleteRuntimeByThreadId(input).pipe( |
| 275 | 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 |