| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Option from "effect/Option"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | import * as SqlClient from "effect/sql/SqlClient"; |
| 7 | import * as SqlSchema from "effect/sql/SqlSchema"; |
| 8 | |
| 9 | import { PullRequestReviewGuide, ThreadPullRequestKey } from "@t3tools/contracts"; |
| 10 | |
| 11 | import { |
| 12 | PersistenceDecodeError, |
| 13 | PersistenceSqlError, |
| 14 | type PullRequestReviewGuideRepositoryError, |
| 15 | } from "./Errors.ts"; |
| 16 | |
| 17 | const GuideRow = Schema.Struct({ |
| 18 | guide: Schema.fromJsonString(PullRequestReviewGuide), |
| 19 | }); |
| 20 | |
| 21 | /** The latest review guide per pull request, keyed by host-level identity. */ |
| 22 | export class PullRequestReviewGuideRepository extends Context.Service< |
| 23 | PullRequestReviewGuideRepository, |
| 24 | { |
| 25 | readonly get: ( |
| 26 | key: ThreadPullRequestKey, |
| 27 | ) => Effect.Effect< |
| 28 | Option.Option<PullRequestReviewGuide>, |
| 29 | PullRequestReviewGuideRepositoryError |
| 30 | >; |
| 31 | readonly put: ( |
| 32 | key: ThreadPullRequestKey, |
| 33 | guide: PullRequestReviewGuide, |
| 34 | ) => Effect.Effect<void, PullRequestReviewGuideRepositoryError>; |
| 35 | } |
| 36 | >()("t3/persistence/PullRequestReviewGuides/PullRequestReviewGuideRepository") {} |
| 37 | |
| 38 | const make = Effect.gen(function* () { |
| 39 | const sql = yield* SqlClient.SqlClient; |
| 40 | |
| 41 | const findGuide = SqlSchema.findOneOption({ |
| 42 | Request: ThreadPullRequestKey, |
| 43 | Result: GuideRow, |
| 44 | execute: ({ host, repository, number }) => |
| 45 | sql` |
| 46 | SELECT guide_json AS "guide" |
| 47 | FROM pull_request_review_guides |
| 48 | WHERE host = ${host} AND repository = ${repository} AND number = ${number} |
| 49 | `, |
| 50 | }); |
| 51 | const encodeGuide = Schema.encodeEffect(Schema.fromJsonString(PullRequestReviewGuide)); |
| 52 | |
| 53 | return PullRequestReviewGuideRepository.of({ |
| 54 | get: (key) => |
| 55 | findGuide(key).pipe( |
| 56 | Effect.map(Option.map((row) => row.guide)), |
| 57 | Effect.mapError((cause) => |
| 58 | Schema.isSchemaError(cause) |
| 59 | ? PersistenceDecodeError.fromSchemaError("PullRequestReviewGuide", cause) |
| 60 | : new PersistenceSqlError({ operation: "getPullRequestReviewGuide", cause }), |
| 61 | ), |
| 62 | ), |
| 63 | put: (key, guide) => |
| 64 | encodeGuide(guide).pipe( |
| 65 | Effect.mapError((cause) => |
| 66 | PersistenceDecodeError.fromSchemaError("PullRequestReviewGuide", cause), |
| 67 | ), |
| 68 | Effect.flatMap((guideJson) => |
| 69 | sql` |
| 70 | INSERT INTO pull_request_review_guides (host, repository, number, guide_json) |
| 71 | VALUES (${key.host}, ${key.repository}, ${key.number}, ${guideJson}) |
| 72 | ON CONFLICT (host, repository, number) DO UPDATE SET guide_json = excluded.guide_json |
| 73 | `.pipe( |
| 74 | Effect.mapError( |
| 75 | (cause) => new PersistenceSqlError({ operation: "putPullRequestReviewGuide", cause }), |
| 76 | ), |
| 77 | ), |
| 78 | ), |
| 79 | Effect.asVoid, |
| 80 | ), |
| 81 | }); |
| 82 | }); |
| 83 | |
| 84 | export const layer = Layer.effect(PullRequestReviewGuideRepository, make); |