1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6import * as SqlClient from "effect/sql/SqlClient";
7import * as SqlSchema from "effect/sql/SqlSchema";
8
9import { PullRequestReviewGuide, ThreadPullRequestKey } from "@t3tools/contracts";
10
11import {
12 PersistenceDecodeError,
13 PersistenceSqlError,
14 type PullRequestReviewGuideRepositoryError,
15} from "./Errors.ts";
16
17const GuideRow = Schema.Struct({
18 guide: Schema.fromJsonString(PullRequestReviewGuide),
19});
20
21/** The latest review guide per pull request, keyed by host-level identity. */
22export 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
38const 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
84export const layer = Layer.effect(PullRequestReviewGuideRepository, make);