pierre-review 0.1.25 → 0.1.27
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/dist/api/routes/feed.js +9 -0
- package/dist/api/routes/prs.js +247 -1
- package/dist/api/routes/threads.js +89 -1
- package/dist/app.js +2 -0
- package/dist/db/migrations/0016_repo_viewer_permission.sql +7 -0
- package/dist/db/migrations/meta/_journal.json +7 -0
- package/dist/db/migrations-pg/0006_majestic_dracula.sql +1 -0
- package/dist/db/migrations-pg/meta/0006_snapshot.json +2207 -0
- package/dist/db/migrations-pg/meta/_journal.json +7 -0
- package/dist/db/queries.js +502 -47
- package/dist/db/schema.pg.js +4 -0
- package/dist/db/schema.sqlite.js +5 -0
- package/dist/github/diff-anchor.js +227 -0
- package/dist/github/mutations.js +178 -0
- package/dist/github/queries.js +1 -0
- package/dist/review/post-review.js +6 -198
- package/dist/sync/sync-repo.js +1 -1
- package/dist/sync/upsert.js +60 -6
- package/package.json +1 -1
- package/public/assets/index-BbKz1lfU.css +10 -0
- package/public/assets/{index-Ch3iNqCy.js → index-miJLSfq1.js} +94 -92
- package/public/index.html +2 -2
- package/public/assets/index-5W0TZ6y7.css +0 -10
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getFeed } from '../../db/queries.js';
|
|
2
|
+
import { accountIdOf } from '../plugins/auth.js';
|
|
3
|
+
// The watched-repo activity Feed: recent events (last 14 days) across repos the user
|
|
4
|
+
// has Watched, newest first, commit pushes excluded. The frontend mirrors these into an
|
|
5
|
+
// append-only IndexedDB store (see lib/feedStore.ts). Account-scoped.
|
|
6
|
+
export async function feedRoutes(app) {
|
|
7
|
+
app.get('/api/feed', async (req) => getFeed(accountIdOf(req), 14));
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=feed.js.map
|
package/dist/api/routes/prs.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getAccessToken, getAccountUserId } from '../../auth/account.js';
|
|
3
|
+
import { getPrDetail, getPrFilesContext, getPrWriteContext, markAllViewed, markPrViewed, upsertLocalPrComment, upsertLocalReview, } from '../../db/queries.js';
|
|
4
|
+
import { buildFileAnchors, fallbackAnchor, isFindingAnchored, } from '../../github/diff-anchor.js';
|
|
5
|
+
import { addIssueComment, fetchHeadShaFor, fetchPrFilesWithPatch, postInlineComment, submitPrReview, } from '../../github/mutations.js';
|
|
2
6
|
import { hydratePrDetail } from '../../sync/hydrate-detail.js';
|
|
3
7
|
import { accountIdOf } from '../plugins/auth.js';
|
|
8
|
+
// GitHub anchors a file in the PR "Files changed" diff by the SHA-256 of its
|
|
9
|
+
// path (matches db/queries.ts + hydrate-detail.ts's diffAnchorId).
|
|
10
|
+
function diffAnchorId(path) {
|
|
11
|
+
return createHash('sha256').update(path, 'utf8').digest('hex');
|
|
12
|
+
}
|
|
13
|
+
const PR_FILE_STATUSES = [
|
|
14
|
+
'added',
|
|
15
|
+
'modified',
|
|
16
|
+
'removed',
|
|
17
|
+
'renamed',
|
|
18
|
+
'changed',
|
|
19
|
+
'copied',
|
|
20
|
+
'unchanged',
|
|
21
|
+
];
|
|
22
|
+
// Pass GitHub's REST file status through verbatim when it's one we model, else
|
|
23
|
+
// fall back to 'changed' (the catch-all GitHub itself uses).
|
|
24
|
+
function normalizeStatus(status) {
|
|
25
|
+
return PR_FILE_STATUSES.includes(status)
|
|
26
|
+
? status
|
|
27
|
+
: 'changed';
|
|
28
|
+
}
|
|
4
29
|
const idParamSchema = {
|
|
5
30
|
params: {
|
|
6
31
|
type: 'object',
|
|
@@ -23,6 +48,37 @@ const markAllViewedSchema = {
|
|
|
23
48
|
properties: { repoIds: { type: 'array', items: { type: 'integer' } } },
|
|
24
49
|
},
|
|
25
50
|
};
|
|
51
|
+
const commentSchema = {
|
|
52
|
+
...idParamSchema,
|
|
53
|
+
body: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
required: ['body'],
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
properties: { body: { type: 'string' } },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const approveSchema = {
|
|
61
|
+
...idParamSchema,
|
|
62
|
+
body: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
properties: { body: { type: 'string' } },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const reviewCommentSchema = {
|
|
69
|
+
...idParamSchema,
|
|
70
|
+
body: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
required: ['path', 'line', 'body'],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
properties: {
|
|
75
|
+
path: { type: 'string' },
|
|
76
|
+
line: { type: 'integer' },
|
|
77
|
+
side: { type: 'string', enum: ['LEFT', 'RIGHT'] },
|
|
78
|
+
body: { type: 'string' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
26
82
|
export async function prRoutes(app) {
|
|
27
83
|
// Bulk "mark all seen": stamp every open PR (optionally scoped to repoIds) viewed
|
|
28
84
|
// at its head, clearing all new-since badges at once. Static path — no :id — so it
|
|
@@ -66,5 +122,195 @@ export async function prRoutes(app) {
|
|
|
66
122
|
}
|
|
67
123
|
return { status: 'ok' };
|
|
68
124
|
});
|
|
125
|
+
// Post a new issue-level (general) PR comment, then optimistically stamp it
|
|
126
|
+
// locally so it shows before the next sync.
|
|
127
|
+
app.post('/api/prs/:id/comment', { schema: commentSchema }, async (req, reply) => {
|
|
128
|
+
const { id } = req.params;
|
|
129
|
+
const { body } = req.body;
|
|
130
|
+
const accountId = accountIdOf(req);
|
|
131
|
+
const ctx = await getPrWriteContext(id, accountId);
|
|
132
|
+
if (!ctx) {
|
|
133
|
+
reply.status(404);
|
|
134
|
+
return { error: 'NotFound', message: `PR ${id} not found` };
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const token = await getAccessToken(accountId);
|
|
138
|
+
const gh = await addIssueComment(token, ctx.owner, ctx.name, ctx.number, body);
|
|
139
|
+
const authorId = await getAccountUserId(accountId);
|
|
140
|
+
const rowId = await upsertLocalPrComment(ctx.prId, authorId, gh);
|
|
141
|
+
const result = {
|
|
142
|
+
id: rowId,
|
|
143
|
+
authorId,
|
|
144
|
+
body: gh.body,
|
|
145
|
+
createdAt: new Date(gh.createdAt).toISOString(),
|
|
146
|
+
url: gh.url,
|
|
147
|
+
};
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
reply.status(502);
|
|
152
|
+
return {
|
|
153
|
+
error: 'GitHubError',
|
|
154
|
+
message: err instanceof Error ? err.message : String(err),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
// Approve the PR. Server re-checks (matching getPrDetail.viewerCanApprove) that
|
|
159
|
+
// the viewer has write+ permission and isn't the author; else 403. On success
|
|
160
|
+
// submits an APPROVE review and stamps it locally. A GitHub 422 (e.g. a
|
|
161
|
+
// self-approve race) bubbles to the 502 catch.
|
|
162
|
+
app.post('/api/prs/:id/approve', { schema: approveSchema }, async (req, reply) => {
|
|
163
|
+
const { id } = req.params;
|
|
164
|
+
const { body } = (req.body ?? {});
|
|
165
|
+
const accountId = accountIdOf(req);
|
|
166
|
+
const ctx = await getPrWriteContext(id, accountId);
|
|
167
|
+
if (!ctx) {
|
|
168
|
+
reply.status(404);
|
|
169
|
+
return { error: 'NotFound', message: `PR ${id} not found` };
|
|
170
|
+
}
|
|
171
|
+
const viewerUserId = await getAccountUserId(accountId);
|
|
172
|
+
const canApprove = viewerUserId != null &&
|
|
173
|
+
viewerUserId !== ctx.authorId &&
|
|
174
|
+
['WRITE', 'MAINTAIN', 'ADMIN'].includes(ctx.viewerPermission ?? '');
|
|
175
|
+
if (!canApprove) {
|
|
176
|
+
reply.status(403);
|
|
177
|
+
return {
|
|
178
|
+
error: 'NotPermitted',
|
|
179
|
+
message: 'You need write access to this repo and cannot approve your own PR.',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const token = await getAccessToken(accountId);
|
|
184
|
+
const gh = await submitPrReview(token, ctx.owner, ctx.name, ctx.number, {
|
|
185
|
+
event: 'APPROVE',
|
|
186
|
+
body,
|
|
187
|
+
});
|
|
188
|
+
const rowId = await upsertLocalReview(ctx.prId, viewerUserId, gh);
|
|
189
|
+
const result = {
|
|
190
|
+
id: rowId,
|
|
191
|
+
authorId: viewerUserId,
|
|
192
|
+
state: 'approved',
|
|
193
|
+
body: gh.body,
|
|
194
|
+
submittedAt: new Date(gh.submittedAt).toISOString(),
|
|
195
|
+
url: gh.url,
|
|
196
|
+
};
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
reply.status(502);
|
|
201
|
+
return {
|
|
202
|
+
error: 'GitHubError',
|
|
203
|
+
message: err instanceof Error ? err.message : String(err),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// Add ONE inline review comment, posted immediately. Validates the requested
|
|
208
|
+
// (path, line, side) lands on an addable diff line; if not, re-anchors to the
|
|
209
|
+
// file's first changed line; if the file has no changes at all, returns a
|
|
210
|
+
// not-anchored result without posting.
|
|
211
|
+
app.post('/api/prs/:id/review-comment', { schema: reviewCommentSchema }, async (req, reply) => {
|
|
212
|
+
const { id } = req.params;
|
|
213
|
+
const { path, line, body } = req.body;
|
|
214
|
+
const side = req.body.side ?? 'RIGHT';
|
|
215
|
+
const accountId = accountIdOf(req);
|
|
216
|
+
const ctx = await getPrWriteContext(id, accountId);
|
|
217
|
+
if (!ctx) {
|
|
218
|
+
reply.status(404);
|
|
219
|
+
return { error: 'NotFound', message: `PR ${id} not found` };
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const token = await getAccessToken(accountId);
|
|
223
|
+
// Resolve the LIVE head (not the possibly-stale DB head): commit_id must
|
|
224
|
+
// pin to the same commit whose diff we validate the line against below,
|
|
225
|
+
// or GitHub 422s the line as "not part of the diff".
|
|
226
|
+
const head = await fetchHeadShaFor(token, ctx.owner, ctx.name, ctx.number);
|
|
227
|
+
// Find the requested file's REST patch (header-less) to validate anchoring.
|
|
228
|
+
const { files } = await fetchPrFilesWithPatch(token, ctx.owner, ctx.name, ctx.number);
|
|
229
|
+
const file = files.find((f) => f.filename === path);
|
|
230
|
+
const anchors = buildFileAnchors(path, file?.patch ?? null);
|
|
231
|
+
// A single-file AnchorIndex for the pure helpers.
|
|
232
|
+
const index = new Map([[path, anchors]]);
|
|
233
|
+
let finalLine = line;
|
|
234
|
+
let finalSide = side;
|
|
235
|
+
let anchored = true;
|
|
236
|
+
if (!isFindingAnchored(index, path, line, side)) {
|
|
237
|
+
const fb = fallbackAnchor(index, path);
|
|
238
|
+
if (!fb) {
|
|
239
|
+
// The file has no changes in the diff → can't post inline.
|
|
240
|
+
const result = {
|
|
241
|
+
commentId: null,
|
|
242
|
+
url: null,
|
|
243
|
+
line,
|
|
244
|
+
side,
|
|
245
|
+
anchored: false,
|
|
246
|
+
};
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
finalLine = fb.line;
|
|
250
|
+
finalSide = fb.side;
|
|
251
|
+
anchored = false;
|
|
252
|
+
}
|
|
253
|
+
const gh = await postInlineComment(token, ctx.owner, ctx.name, ctx.number, { commitId: head, path, line: finalLine, side: finalSide, body });
|
|
254
|
+
const result = {
|
|
255
|
+
commentId: gh.databaseId,
|
|
256
|
+
url: gh.url,
|
|
257
|
+
line: finalLine,
|
|
258
|
+
side: finalSide,
|
|
259
|
+
anchored,
|
|
260
|
+
};
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
265
|
+
// A 422 means GitHub rejected the line as not part of the diff (e.g. the
|
|
266
|
+
// head shifted between our diff fetch and the post). Surface it as the
|
|
267
|
+
// structured "couldn't place" result so the FE's recovery UX engages
|
|
268
|
+
// (open on GitHub) instead of a generic error toast.
|
|
269
|
+
if (/->\s*422\b/.test(message)) {
|
|
270
|
+
const result = {
|
|
271
|
+
commentId: null,
|
|
272
|
+
url: null,
|
|
273
|
+
line,
|
|
274
|
+
side,
|
|
275
|
+
anchored: false,
|
|
276
|
+
};
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
reply.status(502);
|
|
280
|
+
return { error: 'GitHubError', message };
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
// Changes tab: per-file diff patches, loaded on demand. Degrades to an empty
|
|
284
|
+
// list on a GitHub fetch error (never 500s) so the tab fails gracefully.
|
|
285
|
+
app.get('/api/prs/:id/files', { schema: idParamSchema }, async (req, reply) => {
|
|
286
|
+
const { id } = req.params;
|
|
287
|
+
const accountId = accountIdOf(req);
|
|
288
|
+
const ctx = await getPrFilesContext(id, accountId);
|
|
289
|
+
if (!ctx) {
|
|
290
|
+
reply.status(404);
|
|
291
|
+
return { error: 'NotFound', message: `PR ${id} not found` };
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const token = await getAccessToken(accountId);
|
|
295
|
+
const { files, truncated } = await fetchPrFilesWithPatch(token, ctx.owner, ctx.name, ctx.number);
|
|
296
|
+
const mapped = files.map((f) => ({
|
|
297
|
+
path: f.filename,
|
|
298
|
+
previousPath: f.previous_filename ?? null,
|
|
299
|
+
status: normalizeStatus(f.status),
|
|
300
|
+
additions: f.additions,
|
|
301
|
+
deletions: f.deletions,
|
|
302
|
+
patch: f.patch ?? null,
|
|
303
|
+
githubUrl: `${ctx.prUrl}/files#diff-${diffAnchorId(f.filename)}`,
|
|
304
|
+
blobUrl: f.blob_url,
|
|
305
|
+
}));
|
|
306
|
+
const result = { files: mapped, truncated };
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Graceful degrade — the Changes tab shows "no files" rather than 500ing.
|
|
311
|
+
const result = { files: [], truncated: false };
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
69
315
|
}
|
|
70
316
|
//# sourceMappingURL=prs.js.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getAccessToken, getAccountUserId } from '../../auth/account.js';
|
|
2
|
+
import { getThreadDetail, getThreadWriteContext, stampThreadRepliedState, stampThreadResolved, upsertLocalReply, } from '../../db/queries.js';
|
|
3
|
+
import { addReviewThreadReply, setReviewThreadResolved, } from '../../github/mutations.js';
|
|
2
4
|
import { hydrateThreadDetail } from '../../sync/hydrate-detail.js';
|
|
3
5
|
import { accountIdOf } from '../plugins/auth.js';
|
|
4
6
|
const idParamSchema = {
|
|
@@ -8,6 +10,24 @@ const idParamSchema = {
|
|
|
8
10
|
properties: { id: { type: 'integer' } },
|
|
9
11
|
},
|
|
10
12
|
};
|
|
13
|
+
const replySchema = {
|
|
14
|
+
...idParamSchema,
|
|
15
|
+
body: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
required: ['body'],
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: { body: { type: 'string' } },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const resolveSchema = {
|
|
23
|
+
...idParamSchema,
|
|
24
|
+
body: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
required: ['resolved'],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: { resolved: { type: 'boolean' } },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
11
31
|
export async function threadRoutes(app) {
|
|
12
32
|
app.get('/api/threads/:id', { schema: idParamSchema }, async (req, reply) => {
|
|
13
33
|
const { id } = req.params;
|
|
@@ -19,5 +39,73 @@ export async function threadRoutes(app) {
|
|
|
19
39
|
}
|
|
20
40
|
return hydrateThreadDetail(thread, accountId);
|
|
21
41
|
});
|
|
42
|
+
// Reply to an existing review thread. GraphQL addPullRequestReviewThreadReply,
|
|
43
|
+
// then optimistically stamp the new comment locally so it shows before sync.
|
|
44
|
+
app.post('/api/threads/:id/reply', { schema: replySchema }, async (req, reply) => {
|
|
45
|
+
const { id } = req.params;
|
|
46
|
+
const { body } = req.body;
|
|
47
|
+
const accountId = accountIdOf(req);
|
|
48
|
+
const ctx = await getThreadWriteContext(id, accountId);
|
|
49
|
+
if (!ctx) {
|
|
50
|
+
reply.status(404);
|
|
51
|
+
return { error: 'NotFound', message: `Thread ${id} not found` };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const token = await getAccessToken(accountId);
|
|
55
|
+
const gh = await addReviewThreadReply(token, ctx.threadNodeId, body);
|
|
56
|
+
const authorId = await getAccountUserId(accountId);
|
|
57
|
+
const rowId = await upsertLocalReply(ctx.prId, id, authorId, gh);
|
|
58
|
+
// Bump the parent thread off 'untouched' so its badge reflects the reply
|
|
59
|
+
// before the next sync re-derives.
|
|
60
|
+
await stampThreadRepliedState(id);
|
|
61
|
+
const result = {
|
|
62
|
+
id: rowId,
|
|
63
|
+
authorId,
|
|
64
|
+
body: gh.body,
|
|
65
|
+
diffHunk: null,
|
|
66
|
+
createdAt: new Date(gh.createdAt).toISOString(),
|
|
67
|
+
url: gh.url,
|
|
68
|
+
};
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
reply.status(502);
|
|
73
|
+
return {
|
|
74
|
+
error: 'GitHubError',
|
|
75
|
+
message: err instanceof Error ? err.message : String(err),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Resolve (resolved=true) or unresolve (resolved=false) a review thread, then
|
|
80
|
+
// stamp the local derivedState so the UI reflects it before the next sync.
|
|
81
|
+
app.post('/api/threads/:id/resolve', { schema: resolveSchema }, async (req, reply) => {
|
|
82
|
+
const { id } = req.params;
|
|
83
|
+
const { resolved } = req.body;
|
|
84
|
+
const accountId = accountIdOf(req);
|
|
85
|
+
const ctx = await getThreadWriteContext(id, accountId);
|
|
86
|
+
if (!ctx) {
|
|
87
|
+
reply.status(404);
|
|
88
|
+
return { error: 'NotFound', message: `Thread ${id} not found` };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const token = await getAccessToken(accountId);
|
|
92
|
+
const gh = await setReviewThreadResolved(token, ctx.threadNodeId, resolved);
|
|
93
|
+
// Ownership already confirmed above, so the stamp is non-null.
|
|
94
|
+
const derivedState = await stampThreadResolved(id, resolved, accountId);
|
|
95
|
+
const result = {
|
|
96
|
+
threadId: id,
|
|
97
|
+
isResolved: gh.isResolved,
|
|
98
|
+
derivedState: derivedState ?? (resolved ? 'resolved' : 'untouched'),
|
|
99
|
+
};
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
reply.status(502);
|
|
104
|
+
return {
|
|
105
|
+
error: 'GitHubError',
|
|
106
|
+
message: err instanceof Error ? err.message : String(err),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
22
110
|
}
|
|
23
111
|
//# sourceMappingURL=threads.js.map
|
package/dist/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { prRoutes } from './api/routes/prs.js';
|
|
|
15
15
|
import { threadRoutes } from './api/routes/threads.js';
|
|
16
16
|
import { meRoutes } from './api/routes/me.js';
|
|
17
17
|
import { openPrsRoutes } from './api/routes/open-prs.js';
|
|
18
|
+
import { feedRoutes } from './api/routes/feed.js';
|
|
18
19
|
import { mergersRoutes } from './api/routes/mergers.js';
|
|
19
20
|
import { insightsRoutes } from './api/routes/insights.js';
|
|
20
21
|
import { claudeReviewRoutes } from './api/routes/claude-review.js';
|
|
@@ -126,6 +127,7 @@ export async function buildApp() {
|
|
|
126
127
|
await app.register(threadRoutes);
|
|
127
128
|
await app.register(meRoutes);
|
|
128
129
|
await app.register(openPrsRoutes);
|
|
130
|
+
await app.register(feedRoutes);
|
|
129
131
|
await app.register(mergersRoutes);
|
|
130
132
|
await app.register(insightsRoutes);
|
|
131
133
|
// Claude Review is local-only + opt-in. Only register its routes when enabled,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Viewer's repo permission (additive). `viewer_permission` records the GraphQL
|
|
2
|
+
-- Repository.viewerPermission enum (ADMIN/MAINTAIN/WRITE/TRIAGE/READ) captured each
|
|
3
|
+
-- activity sync. It drives whether the viewer may approve a PR (WRITE+ and not the
|
|
4
|
+
-- author) — surfaced as PrDetail.viewerCanApprove. Nullable; existing rows stay NULL
|
|
5
|
+
-- until the next sync repopulates them. Postgres baseline is regenerated separately
|
|
6
|
+
-- via db:generate:pg.
|
|
7
|
+
ALTER TABLE `repos` ADD `viewer_permission` text;
|
|
@@ -113,6 +113,13 @@
|
|
|
113
113
|
"when": 1780800000006,
|
|
114
114
|
"tag": "0015_repos_inbox_watch",
|
|
115
115
|
"breakpoints": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"idx": 16,
|
|
119
|
+
"version": "6",
|
|
120
|
+
"when": 1780800000007,
|
|
121
|
+
"tag": "0016_repo_viewer_permission",
|
|
122
|
+
"breakpoints": true
|
|
116
123
|
}
|
|
117
124
|
]
|
|
118
125
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "repos" ADD COLUMN "viewer_permission" text;
|