@catalyst-cloud/schema 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +21 -0
- package/src/migrations.generated.ts +18 -0
- package/src/mirror.ts +186 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/schema",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed Drizzle schema = single source of truth for the per-tenant Mirror DO SQLite store (CTC-13 / ADR-0002). Shared by the mirror Worker, the host-sync replica, and the browser OPFS replica.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
issue_history,
|
|
19
19
|
projects,
|
|
20
20
|
cycles,
|
|
21
|
+
workflow_states,
|
|
21
22
|
initiatives,
|
|
22
23
|
project_initiatives,
|
|
23
24
|
comments,
|
|
@@ -35,6 +36,11 @@ import {
|
|
|
35
36
|
fleet_activity_pruned,
|
|
36
37
|
pull_sweeps,
|
|
37
38
|
pr_review_backfill,
|
|
39
|
+
pr_review_comments,
|
|
40
|
+
pr_conversation_comments,
|
|
41
|
+
pr_commits,
|
|
42
|
+
pr_events,
|
|
43
|
+
pr_ancillary_backfill,
|
|
38
44
|
} from "./mirror.js";
|
|
39
45
|
|
|
40
46
|
export * from "./mirror.js";
|
|
@@ -71,6 +77,7 @@ export const mirrorSchema = {
|
|
|
71
77
|
issue_history,
|
|
72
78
|
projects,
|
|
73
79
|
cycles,
|
|
80
|
+
workflow_states,
|
|
74
81
|
initiatives,
|
|
75
82
|
project_initiatives,
|
|
76
83
|
comments,
|
|
@@ -97,6 +104,14 @@ export const mirrorSchema = {
|
|
|
97
104
|
agent_sessions,
|
|
98
105
|
agent_activities,
|
|
99
106
|
fleet_activity_pruned,
|
|
107
|
+
// CTC-575/577: inline review comments, PR conversation comments, PR commits, PR timeline events —
|
|
108
|
+
// all four ARE change-feed entities (see mirrorEntityTables below). The fifth new table,
|
|
109
|
+
// pr_ancillary_backfill, is hub-read-only like pr_review_backfill (see its own doc).
|
|
110
|
+
pr_review_comments,
|
|
111
|
+
pr_conversation_comments,
|
|
112
|
+
pr_commits,
|
|
113
|
+
pr_events,
|
|
114
|
+
pr_ancillary_backfill,
|
|
100
115
|
} as const;
|
|
101
116
|
|
|
102
117
|
/**
|
|
@@ -123,6 +138,7 @@ export const mirrorEntityTables = {
|
|
|
123
138
|
issue_history,
|
|
124
139
|
projects,
|
|
125
140
|
cycles,
|
|
141
|
+
workflow_states,
|
|
126
142
|
initiatives,
|
|
127
143
|
project_initiatives,
|
|
128
144
|
comments,
|
|
@@ -136,6 +152,11 @@ export const mirrorEntityTables = {
|
|
|
136
152
|
// CTC-524: agent sessions + their activities.
|
|
137
153
|
agent_sessions,
|
|
138
154
|
agent_activities,
|
|
155
|
+
// CTC-575/577: inline review comments, PR conversation comments, PR commits, PR timeline events.
|
|
156
|
+
pr_review_comments,
|
|
157
|
+
pr_conversation_comments,
|
|
158
|
+
pr_commits,
|
|
159
|
+
pr_events,
|
|
139
160
|
} as const;
|
|
140
161
|
|
|
141
162
|
/** The wire `entity` name of a change-feed-carried table (keys of mirrorEntityTables). */
|
|
@@ -158,6 +158,20 @@ export const MIRROR_MIGRATIONS = {
|
|
|
158
158
|
tag: "0020_wealthy_snowbird",
|
|
159
159
|
breakpoints: true,
|
|
160
160
|
},
|
|
161
|
+
{
|
|
162
|
+
idx: 21,
|
|
163
|
+
version: "6",
|
|
164
|
+
when: 1786857732587,
|
|
165
|
+
tag: "0021_worried_annihilus",
|
|
166
|
+
breakpoints: true,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
idx: 22,
|
|
170
|
+
version: "6",
|
|
171
|
+
when: 1786928302892,
|
|
172
|
+
tag: "0022_chilly_gorgon",
|
|
173
|
+
breakpoints: true,
|
|
174
|
+
},
|
|
161
175
|
],
|
|
162
176
|
},
|
|
163
177
|
migrations: {
|
|
@@ -201,5 +215,9 @@ export const MIRROR_MIGRATIONS = {
|
|
|
201
215
|
"ALTER TABLE `agent_sessions` ADD `archived_at` integer;--> statement-breakpoint\nALTER TABLE `agent_sessions` ADD `dismissed_at` integer;--> statement-breakpoint\nALTER TABLE `agent_sessions` ADD `dismissed_by` text;",
|
|
202
216
|
"0020_wealthy_snowbird":
|
|
203
217
|
"CREATE TABLE `pr_review_backfill` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`backfilled_at` integer NOT NULL,\n\tPRIMARY KEY(`repo_id`, `pr_number`)\n);\n--> statement-breakpoint\nALTER TABLE `reviews` ADD `body` text;",
|
|
218
|
+
"0021_worried_annihilus":
|
|
219
|
+
"CREATE TABLE `pr_ancillary_backfill` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`backfilled_at` integer NOT NULL,\n\tPRIMARY KEY(`repo_id`, `pr_number`)\n);\n--> statement-breakpoint\nCREATE TABLE `pr_commits` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`sha` text NOT NULL,\n\t`message` text,\n\t`author_login` text,\n\t`author_name` text,\n\t`author_email` text,\n\t`authored_at` integer,\n\t`committed_at` integer,\n\tPRIMARY KEY(`repo_id`, `pr_number`, `sha`)\n);\n--> statement-breakpoint\nCREATE TABLE `pr_conversation_comments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`author_id` text,\n\t`body` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`removed_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_conversation_comments_pr` ON `pr_conversation_comments` (`repo_id`,`pr_number`);--> statement-breakpoint\nCREATE TABLE `pr_events` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`action` text,\n\t`actor_id` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`raw` text\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_events_pr` ON `pr_events` (`repo_id`,`pr_number`,`created_at`);--> statement-breakpoint\nCREATE TABLE `pr_review_comments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`review_id` text,\n\t`commit_id` text,\n\t`path` text,\n\t`line` integer,\n\t`diff_hunk` text,\n\t`in_reply_to_id` text,\n\t`author_id` text,\n\t`body` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`removed_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_review_comments_pr` ON `pr_review_comments` (`repo_id`,`pr_number`);",
|
|
220
|
+
"0022_chilly_gorgon":
|
|
221
|
+
"CREATE TABLE `workflow_states` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`team_id` text,\n\t`name` text,\n\t`type` text,\n\t`position` real,\n\t`color` text,\n\t`archived_at` integer,\n\t`updated_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_workflow_states_team` ON `workflow_states` (`team_id`);--> statement-breakpoint\nALTER TABLE `issues` ADD `state_type` text;--> statement-breakpoint\nALTER TABLE `issues` ADD `state_position` real;",
|
|
204
222
|
},
|
|
205
223
|
} as const;
|
package/src/mirror.ts
CHANGED
|
@@ -39,6 +39,11 @@ export const issues = sqliteTable(
|
|
|
39
39
|
// CTC-148: the workflow state id behind the `state` NAME — lets a consumer key on state without
|
|
40
40
|
// string-matching the renameable name.
|
|
41
41
|
state_id: text("state_id"),
|
|
42
|
+
// CTC-308: the state's bucket (triage|backlog|unstarted|started|completed|canceled) + its manual
|
|
43
|
+
// column order — denormalized off `state` (the `workflow_states` FK, below) so a board can order
|
|
44
|
+
// its columns without a join on the hot path. Null on a partial webhook, exactly like state_id.
|
|
45
|
+
state_type: text("state_type"),
|
|
46
|
+
state_position: real("state_position"),
|
|
42
47
|
assignee: text("assignee"),
|
|
43
48
|
// CTC-USERS: the assignee's user id (FK → users.id), so the row JOINs to a name + avatar. The
|
|
44
49
|
// legacy `assignee` name string is KEPT for back-compat (the SSE list row still carries it).
|
|
@@ -308,6 +313,28 @@ export const cycles = sqliteTable("cycles", {
|
|
|
308
313
|
updated_at: integer("updated_at"),
|
|
309
314
|
});
|
|
310
315
|
|
|
316
|
+
// Linear workflow states (CTC-308) — the id/type/position behind an issue's `state` NAME, mirrored as
|
|
317
|
+
// its own entity table so a board can order columns by (type, position) and key a column across teams
|
|
318
|
+
// (the prerequisite CTC-277's kanban board blocks on). Small, workspace-wide set — full sweep each
|
|
319
|
+
// pass, same discipline as `labels`/`cycles`. No `removed_at`: Linear ARCHIVES a workflow state rather
|
|
320
|
+
// than deleting it (`archived_at`), same convention `issues.archived_at` already uses.
|
|
321
|
+
export const workflow_states = sqliteTable(
|
|
322
|
+
"workflow_states",
|
|
323
|
+
{
|
|
324
|
+
id: text("id").primaryKey(),
|
|
325
|
+
team_id: text("team_id"),
|
|
326
|
+
name: text("name"),
|
|
327
|
+
// The state's bucket: triage | backlog | unstarted | started | completed | canceled.
|
|
328
|
+
type: text("type"),
|
|
329
|
+
// Manual sort rank within its team's workflow — a Float, Linear's own column order.
|
|
330
|
+
position: real("position"),
|
|
331
|
+
color: text("color"),
|
|
332
|
+
archived_at: integer("archived_at"),
|
|
333
|
+
updated_at: integer("updated_at"),
|
|
334
|
+
},
|
|
335
|
+
(t) => [index("idx_workflow_states_team").on(t.team_id)],
|
|
336
|
+
);
|
|
337
|
+
|
|
311
338
|
// Linear initiatives (CTC-HIERARCHY) — the top of the HIERARCHY (initiative → project → cycle →
|
|
312
339
|
// issue). PK `id` (Linear node id). `status` is the InitiativeStatus enum string; `target_date` is a
|
|
313
340
|
// TimelessDate ("YYYY-MM-DD") stored AS-IS as TEXT (the trap). `owner_id` is the FK → users.id.
|
|
@@ -446,6 +473,135 @@ export const reviews = sqliteTable("reviews", {
|
|
|
446
473
|
body: text("body"),
|
|
447
474
|
});
|
|
448
475
|
|
|
476
|
+
/**
|
|
477
|
+
* CTC-575 — inline PR review comments (the diff-attached kind, `GET /pulls/{n}/comments` +
|
|
478
|
+
* the `pull_request_review_comment` webhook). Distinct from {@link pr_conversation_comments} below —
|
|
479
|
+
* GitHub itself models these as two different REST resources and two different webhook events; this
|
|
480
|
+
* table only ever holds the diff-anchored kind (`path`/`line`/`diff_hunk` are meaningful here and
|
|
481
|
+
* absent on a conversation comment).
|
|
482
|
+
*
|
|
483
|
+
* `updated_at` IS present (unlike `reviews`) — GitHub bumps it on an edit, so the ordinary
|
|
484
|
+
* `upsertRow` last-write-wins guard is live here; no hand-rolled read-before-emit needed.
|
|
485
|
+
* Soft-delete (`removed_at`): the webhook sends `action: "deleted"` for a genuinely deleted comment.
|
|
486
|
+
*/
|
|
487
|
+
export const pr_review_comments = sqliteTable(
|
|
488
|
+
"pr_review_comments",
|
|
489
|
+
{
|
|
490
|
+
id: text("id").primaryKey(),
|
|
491
|
+
repo_id: text("repo_id").notNull(),
|
|
492
|
+
pr_number: integer("pr_number").notNull(),
|
|
493
|
+
// The review this comment belongs to (GitHub's pull_request_review_id) — FK → reviews.review_id.
|
|
494
|
+
// Nullable: GitHub's own field is optional on some legacy/edge payloads.
|
|
495
|
+
review_id: text("review_id"),
|
|
496
|
+
commit_id: text("commit_id"),
|
|
497
|
+
path: text("path"),
|
|
498
|
+
line: integer("line"),
|
|
499
|
+
diff_hunk: text("diff_hunk"),
|
|
500
|
+
// Threading: the parent comment id when this is a reply. Nullable — most comments are top-level.
|
|
501
|
+
in_reply_to_id: text("in_reply_to_id"),
|
|
502
|
+
// The resolvable `github:<login>` users key (CTC-USERS), same convention as reviews.user_id.
|
|
503
|
+
author_id: text("author_id"),
|
|
504
|
+
body: text("body"),
|
|
505
|
+
created_at: integer("created_at"),
|
|
506
|
+
updated_at: integer("updated_at"),
|
|
507
|
+
removed_at: integer("removed_at"),
|
|
508
|
+
},
|
|
509
|
+
(t) => [index("idx_pr_review_comments_pr").on(t.repo_id, t.pr_number)],
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* CTC-577 — PR conversation-tab comments (`GET /issues/{n}/comments` + the `issue_comment` webhook,
|
|
514
|
+
* filtered to deliveries whose `issue.pull_request` is present — GitHub fires `issue_comment` for
|
|
515
|
+
* BOTH a plain repo issue comment and a PR conversation comment, and does not distinguish them at the
|
|
516
|
+
* event-type level; the payload's `issue.pull_request` field is the only signal).
|
|
517
|
+
*
|
|
518
|
+
* ⚠️ DELIBERATELY A SEPARATE TABLE FROM LINEAR'S `comments`. `comments` is keyed `issue_id` for
|
|
519
|
+
* LINEAR issues, with no `source`/`repo_id`/`pr_number` column — reusing it would have been a real
|
|
520
|
+
* schema change (a new discriminator + nullable Linear-only columns going unused on every GitHub row),
|
|
521
|
+
* not a free ride. A separate table makes "a Linear comment and a PR comment are never confused" true
|
|
522
|
+
* by construction: there is no shared row space for them to collide in.
|
|
523
|
+
*/
|
|
524
|
+
export const pr_conversation_comments = sqliteTable(
|
|
525
|
+
"pr_conversation_comments",
|
|
526
|
+
{
|
|
527
|
+
id: text("id").primaryKey(),
|
|
528
|
+
repo_id: text("repo_id").notNull(),
|
|
529
|
+
pr_number: integer("pr_number").notNull(),
|
|
530
|
+
author_id: text("author_id"),
|
|
531
|
+
body: text("body"),
|
|
532
|
+
created_at: integer("created_at"),
|
|
533
|
+
updated_at: integer("updated_at"),
|
|
534
|
+
removed_at: integer("removed_at"),
|
|
535
|
+
},
|
|
536
|
+
(t) => [index("idx_pr_conversation_comments_pr").on(t.repo_id, t.pr_number)],
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* CTC-575 — commits on a PR (`GET /pulls/{n}/commits`). The mirror previously stored only the scalar
|
|
541
|
+
* `pull_requests.head_sha`; this is the full commit list. Composite PK (repo_id, pr_number, sha): the
|
|
542
|
+
* SAME commit sha can legitimately appear under more than one PR (e.g. a shared base branch, or a
|
|
543
|
+
* commit cherry-picked into a second PR), so the PR is part of the identity, not just the commit.
|
|
544
|
+
*
|
|
545
|
+
* NO `updated_at` and NO `removed_at`, deliberately: a git commit's content is immutable once
|
|
546
|
+
* authored — there is no legitimate "edit" of a stored row, only a fresh INSERT for a sha not seen
|
|
547
|
+
* before. (A force-push that drops a commit from the PR's list leaves a stale row here, same posture
|
|
548
|
+
* as `check_runs`/`commit_statuses` never retracting a run/status GitHub stops reporting.)
|
|
549
|
+
*/
|
|
550
|
+
export const pr_commits = sqliteTable(
|
|
551
|
+
"pr_commits",
|
|
552
|
+
{
|
|
553
|
+
repo_id: text("repo_id").notNull(),
|
|
554
|
+
pr_number: integer("pr_number").notNull(),
|
|
555
|
+
sha: text("sha").notNull(),
|
|
556
|
+
message: text("message"),
|
|
557
|
+
// The resolvable `github:<login>` users key when GitHub matched the commit author to an account;
|
|
558
|
+
// null for an unmatched/external author (common on a rebased or externally-authored commit).
|
|
559
|
+
author_login: text("author_login"),
|
|
560
|
+
// The raw git author name/email — kept even when author_login is null, so the commit is never
|
|
561
|
+
// author-less in the UI just because GitHub couldn't match it to an account.
|
|
562
|
+
author_name: text("author_name"),
|
|
563
|
+
author_email: text("author_email"),
|
|
564
|
+
authored_at: integer("authored_at"),
|
|
565
|
+
committed_at: integer("committed_at"),
|
|
566
|
+
},
|
|
567
|
+
(t) => [primaryKey({ columns: [t.repo_id, t.pr_number, t.sha] })],
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* CTC-575 — a PR's timeline events (immutable, `issue_history`-shaped), capturing the `action` field
|
|
572
|
+
* that every `pull_request` webhook already carries and that normalizePullRequest has always
|
|
573
|
+
* discarded. Curated to the FIVE actions the ticket names — ready_for_review, merged, closed,
|
|
574
|
+
* reopened, review_requested — not every `pull_request` action GitHub sends: `synchronize` fires on
|
|
575
|
+
* every push and `labeled`/`unlabeled`/`edited` etc. are not "lifecycle" events, so capturing every
|
|
576
|
+
* action verbatim would flood this table with noise the ticket never asked for. `merged` is OUR OWN
|
|
577
|
+
* vocabulary layered on GitHub's `action: "closed"` + `pull_request.merged: true` — GitHub has no
|
|
578
|
+
* separate "merged" action.
|
|
579
|
+
*
|
|
580
|
+
* ⛔ WEBHOOK-ONLY, NO BACKFILL. Unlike the three tables above, there is no REST endpoint that
|
|
581
|
+
* reconstructs this history for a PR predating the webhook subscription — GitHub's PR object carries
|
|
582
|
+
* no event log, only the current state. A PR's timeline before this ships is simply not recoverable.
|
|
583
|
+
*
|
|
584
|
+
* `id` is a SYNTHETIC key (repo_id:pr_number:action:updated_at ms) — GitHub's `pull_request` webhook
|
|
585
|
+
* carries no event-level id the way Linear's IssueHistory nodes do. HARD upsert (no removed_at, like
|
|
586
|
+
* issue_history) — an event, once recorded, never changes or vanishes.
|
|
587
|
+
*/
|
|
588
|
+
export const pr_events = sqliteTable(
|
|
589
|
+
"pr_events",
|
|
590
|
+
{
|
|
591
|
+
id: text("id").primaryKey(),
|
|
592
|
+
repo_id: text("repo_id").notNull(),
|
|
593
|
+
pr_number: integer("pr_number").notNull(),
|
|
594
|
+
// ready_for_review | merged | closed | reopened | review_requested (see the curation note above).
|
|
595
|
+
action: text("action"),
|
|
596
|
+
// Who triggered it (the webhook's top-level `sender`) — the resolvable `github:<login>` key.
|
|
597
|
+
actor_id: text("actor_id"),
|
|
598
|
+
created_at: integer("created_at"), // event time (ms epoch) — the timeline sort key
|
|
599
|
+
updated_at: integer("updated_at"), // = created_at (immutable) — drives the upsert guard
|
|
600
|
+
raw: text("raw"), // JSON.stringify of the relevant webhook fields — full fidelity / forward-compat
|
|
601
|
+
},
|
|
602
|
+
(t) => [index("idx_pr_events_pr").on(t.repo_id, t.pr_number, t.created_at)],
|
|
603
|
+
);
|
|
604
|
+
|
|
449
605
|
// ── Mirror infra (not domain entities — never ride the change-feed) ─────────────────────────────────
|
|
450
606
|
|
|
451
607
|
export const processed_events = sqliteTable("processed_events", {
|
|
@@ -665,3 +821,33 @@ export const pr_review_backfill = sqliteTable(
|
|
|
665
821
|
},
|
|
666
822
|
(t) => [primaryKey({ columns: [t.repo_id, t.pr_number] })],
|
|
667
823
|
);
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* CTC-575/577 — a marker that a PR's ANCILLARY GitHub entities (review comments, commits,
|
|
827
|
+
* conversation comments) have been fetched at least once. Unlike {@link pr_review_backfill} (which
|
|
828
|
+
* tracks a NARROWER re-fetch of already-existing review rows for their new `body` column), this
|
|
829
|
+
* marker covers three BRAND-NEW tables that start with zero rows for every PR, open or closed — so
|
|
830
|
+
* the ancillary backfill walks the WHOLE PR list (not just closed ones), gated per-PR by this marker
|
|
831
|
+
* so a PR already caught up is never re-swept.
|
|
832
|
+
*
|
|
833
|
+
* ⚠️ DOES NOT COVER `pr_events` — that table is webhook-only (see its own doc: no REST endpoint can
|
|
834
|
+
* reconstruct PR timeline history), so there is nothing for a backfill to fetch.
|
|
835
|
+
*
|
|
836
|
+
* ⚠️ `pr_commits` HAS NO WEBHOOK, so a PR's commit list is only ever as fresh as its last backfill —
|
|
837
|
+
* an open PR that receives new commits AFTER being marked backfilled does not get them until this
|
|
838
|
+
* marker is cleared. Documented trade-off (the ticket asks for a backfill, not continuous freshness);
|
|
839
|
+
* a future ticket that wants `pr_commits` live on open PRs should sweep it in step 2 as a peer of
|
|
840
|
+
* checks/statuses/reviews.
|
|
841
|
+
*
|
|
842
|
+
* ⛔ HUB-READ-ONLY, same posture as `pr_review_backfill` / `pull_sweeps`: never rides the change feed.
|
|
843
|
+
*/
|
|
844
|
+
export const pr_ancillary_backfill = sqliteTable(
|
|
845
|
+
"pr_ancillary_backfill",
|
|
846
|
+
{
|
|
847
|
+
repo_id: text("repo_id").notNull(),
|
|
848
|
+
pr_number: integer("pr_number").notNull(),
|
|
849
|
+
/** When the backfill swept this PR's review comments + commits + conversation comments (ms). */
|
|
850
|
+
backfilled_at: integer("backfilled_at").notNull(),
|
|
851
|
+
},
|
|
852
|
+
(t) => [primaryKey({ columns: [t.repo_id, t.pr_number] })],
|
|
853
|
+
);
|