@hollware/server 0.5.0 → 0.6.0
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/{ceremonies-BM9G03lZ.mjs → ceremonies-6mZ3u32c.mjs} +1 -1
- package/dist/index.d.mts +33 -1
- package/dist/index.mjs +156 -23
- package/dist/{store-DiS8UFI2.mjs → store-CYMjP0iN.mjs} +34 -1
- package/migrations/0010_review_comments.sql +17 -0
- package/migrations/meta/0010_snapshot.json +2388 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +3 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as findInvitation, c as invitationById, d as redeemInvitation, f as saveChallenge, h as userHandle, i as createUser, m as touchPasskey, o as findPasskey, p as takeChallenge, r as createSession, t as addPasskey, u as passkeysOf } from "./store-
|
|
1
|
+
import { a as findInvitation, c as invitationById, d as redeemInvitation, f as saveChallenge, h as userHandle, i as createUser, m as touchPasskey, o as findPasskey, p as takeChallenge, r as createSession, t as addPasskey, u as passkeysOf } from "./store-CYMjP0iN.mjs";
|
|
2
2
|
import { HollwareError, newId } from "@hollware/core";
|
|
3
3
|
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
|
|
4
4
|
import { isoBase64URL } from "@simplewebauthn/server/helpers";
|
package/dist/index.d.mts
CHANGED
|
@@ -161,6 +161,17 @@ interface StoredReview {
|
|
|
161
161
|
readonly decidedAt: string | null;
|
|
162
162
|
readonly note: string | null;
|
|
163
163
|
}
|
|
164
|
+
/** A comment on a review (v0.6). */
|
|
165
|
+
interface StoredReviewComment {
|
|
166
|
+
readonly id: string;
|
|
167
|
+
readonly reviewId: string;
|
|
168
|
+
readonly authorId: string;
|
|
169
|
+
readonly authorName: string | null;
|
|
170
|
+
/** What the client that wrote it said about itself, or `null`. */
|
|
171
|
+
readonly authorMeta: Readonly<Record<string, unknown>> | null;
|
|
172
|
+
readonly body: string;
|
|
173
|
+
readonly createdAt: string;
|
|
174
|
+
}
|
|
164
175
|
/** A scheduled publication, as the cron trigger reads it. */
|
|
165
176
|
interface StoredSchedule {
|
|
166
177
|
readonly entryId: string;
|
|
@@ -295,6 +306,19 @@ interface ContentStore {
|
|
|
295
306
|
readonly status: ReviewStatus;
|
|
296
307
|
readonly limit: number;
|
|
297
308
|
}): Promise<readonly StoredReview[]>;
|
|
309
|
+
/** Adds a comment to a review, if it is still pending. */
|
|
310
|
+
commentReview(site: string, write: {
|
|
311
|
+
readonly reviewId: string;
|
|
312
|
+
readonly actor: Actor;
|
|
313
|
+
readonly body: string;
|
|
314
|
+
}): Promise<{
|
|
315
|
+
readonly outcome: 'commented';
|
|
316
|
+
readonly comment: StoredReviewComment;
|
|
317
|
+
} | {
|
|
318
|
+
readonly outcome: 'not_pending' | 'not_found';
|
|
319
|
+
}>;
|
|
320
|
+
/** The comments of these reviews, oldest first, by review. */
|
|
321
|
+
reviewComments(site: string, reviewIds: readonly string[]): Promise<ReadonlyMap<string, readonly StoredReviewComment[]>>;
|
|
298
322
|
/** Closes a pending review with a note, without publishing. */
|
|
299
323
|
requestChanges(site: string, write: {
|
|
300
324
|
readonly id: string;
|
|
@@ -547,6 +571,14 @@ export interface HollwareEnv {
|
|
|
547
571
|
readonly HOLLWARE_ANALYTICS_ACCOUNT_ID?: string;
|
|
548
572
|
readonly HOLLWARE_ANALYTICS_TOKEN?: string;
|
|
549
573
|
readonly HOLLWARE_ANALYTICS_DATASET?: string;
|
|
574
|
+
/**
|
|
575
|
+
* The agents of @hollware/agents (v0.6), over a service binding to their named entrypoint: the
|
|
576
|
+
* cron trigger of Hollware wakes them every ten minutes, so they need no cron trigger of their
|
|
577
|
+
* own (the free plan has five for the account). `wrangler types` writes it as a plain
|
|
578
|
+
* `Service`, as it cannot see the agents' entrypoint, so any binding goes in: one without `run`
|
|
579
|
+
* is not called.
|
|
580
|
+
*/
|
|
581
|
+
readonly AGENTS?: object;
|
|
550
582
|
}
|
|
551
583
|
type Output<Spec extends {
|
|
552
584
|
output: z.ZodType;
|
|
@@ -602,4 +634,4 @@ export type CreateHollware = (config: HollwareConfig) => HollwareWorker;
|
|
|
602
634
|
*/
|
|
603
635
|
export declare const createHollware: CreateHollware;
|
|
604
636
|
//#endregion
|
|
605
|
-
export type { Actor, AnalyticsReader, AnalyticsWriter, Clock, ContentStore, Embedder, EntrySummaryView, EntryView, EventMessage, Handlers, ListQuery, NewRevision, OperationContext, OperationHandler, OutboxEvent, PageCount, PageView, Principal, PublishWrite, RelatedEntry, ReviewStatus, RevisionSummary, SearchHit, SearchQuery, Semantic, StoredEntry, StoredPublication, StoredReview, StoredRevision, StoredSchedule, StoredVector, VectorIndex, VectorMatch, VectorMetadata, View, WriteRefusal };
|
|
637
|
+
export type { Actor, AnalyticsReader, AnalyticsWriter, Clock, ContentStore, Embedder, EntrySummaryView, EntryView, EventMessage, Handlers, ListQuery, NewRevision, OperationContext, OperationHandler, OutboxEvent, PageCount, PageView, Principal, PublishWrite, RelatedEntry, ReviewStatus, RevisionSummary, SearchHit, SearchQuery, Semantic, StoredEntry, StoredPublication, StoredReview, StoredReviewComment, StoredRevision, StoredSchedule, StoredVector, VectorIndex, VectorMatch, VectorMetadata, View, WriteRefusal };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as users, C as reviewComments, D as schedules, E as revisions, M as effectiveScopes, N as isRole, O as sites, S as publications, T as revisionTags, _ as entries, b as members, g as deliveries, j as ROLE_SCOPES, k as tokens, l as membershipsOf, n as createInvitation, s as findSession, v as entryEmbeddings, w as reviews, x as outbox, y as invitations } from "./store-CYMjP0iN.mjs";
|
|
2
2
|
import { CHUNKER_VERSION, EVENT_ID_HEADER, HollwareError, OPERATIONS, SCOPES, SIGNATURE_HEADER, TIMESTAMP_HEADER, chunksOf, describeBuiltInContracts, describeResolvedSite, describeSite, generatePersonalToken, generateReadToken, getEntry, getTranslations, hashPersonalToken, isPersonalToken, isReadToken, isScope, isSiteKey, listEntries, newId, processMarkdown, publish, renderHtml, resolveConfig, runContracts, search, signWebhook, sizeLimitViolation, uniqueSlugViolation, uniqueTranslationViolation } from "@hollware/core";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { drizzle } from "drizzle-orm/d1";
|
|
5
|
-
import { and, desc, eq, getTableColumns, gt, inArray, isNotNull, isNull, lte, ne, or, sql } from "drizzle-orm";
|
|
5
|
+
import { and, asc, desc, eq, getTableColumns, gt, inArray, isNotNull, isNull, lte, ne, or, sql } from "drizzle-orm";
|
|
6
6
|
import { alias } from "drizzle-orm/sqlite-core";
|
|
7
7
|
import { AuthorizationError, CimdFetchError, OAuthAuthorizationServer, OAuthResourceServer, authorizationErrorRedirect } from "@cloudflare/workers-oauth-provider";
|
|
8
8
|
import { getCookie } from "hono/cookie";
|
|
@@ -10,7 +10,7 @@ import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, McpServer, accepted
|
|
|
10
10
|
import * as z from "zod";
|
|
11
11
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
12
12
|
//#region package.json
|
|
13
|
-
var version = "0.
|
|
13
|
+
var version = "0.6.0";
|
|
14
14
|
//#endregion
|
|
15
15
|
//#region src/auth/responses.ts
|
|
16
16
|
/** The challenge of every 401 and 403 (RFC 6750). */
|
|
@@ -1041,7 +1041,7 @@ async function pendingOf(db, site, entryId) {
|
|
|
1041
1041
|
const [row] = await db.select(COLUMNS).from(reviews).innerJoin(users, eq(users.id, reviews.requestedBy)).where(and(eq(reviews.siteId, site), eq(reviews.entryId, entryId), eq(reviews.status, "pending"))).limit(1);
|
|
1042
1042
|
return row === void 0 ? null : toReview(row);
|
|
1043
1043
|
}
|
|
1044
|
-
async function requestReview(db, clock, site, write, whyNothingChanged) {
|
|
1044
|
+
async function requestReview$1(db, clock, site, write, whyNothingChanged) {
|
|
1045
1045
|
const now = clock.now().toISOString();
|
|
1046
1046
|
const row = {
|
|
1047
1047
|
id: newId("rvw"),
|
|
@@ -1103,6 +1103,59 @@ async function requestChanges$1(db, clock, site, write) {
|
|
|
1103
1103
|
review
|
|
1104
1104
|
};
|
|
1105
1105
|
}
|
|
1106
|
+
function metaOf(text) {
|
|
1107
|
+
if (text === null) return null;
|
|
1108
|
+
const parsed = JSON.parse(text);
|
|
1109
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? Object.fromEntries(Object.entries(parsed)) : null;
|
|
1110
|
+
}
|
|
1111
|
+
const COMMENT_COLUMNS = {
|
|
1112
|
+
...getTableColumns(reviewComments),
|
|
1113
|
+
authorName: users.name
|
|
1114
|
+
};
|
|
1115
|
+
function toComment(row) {
|
|
1116
|
+
return {
|
|
1117
|
+
id: row.id,
|
|
1118
|
+
reviewId: row.reviewId,
|
|
1119
|
+
authorId: row.authorId,
|
|
1120
|
+
authorName: row.authorName,
|
|
1121
|
+
authorMeta: metaOf(row.authorMeta),
|
|
1122
|
+
body: row.body,
|
|
1123
|
+
createdAt: row.createdAt
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Adds a comment to a review in one statement that inserts only while the review is pending, so a
|
|
1128
|
+
* decision that lands first leaves no comment behind it.
|
|
1129
|
+
*/
|
|
1130
|
+
async function commentReview$1(db, clock, site, write) {
|
|
1131
|
+
const id = newId("rvc");
|
|
1132
|
+
const now = clock.now().toISOString();
|
|
1133
|
+
const meta = write.actor.meta === null ? null : JSON.stringify(write.actor.meta);
|
|
1134
|
+
if ((await db.all(sql`
|
|
1135
|
+
insert into ${reviewComments}
|
|
1136
|
+
(id, site_id, review_id, author_id, author_meta, body, created_at, updated_at)
|
|
1137
|
+
select ${id}, ${site}, ${write.reviewId}, ${write.actor.userId}, ${meta}, ${write.body}, ${now}, ${now}
|
|
1138
|
+
where exists (select 1 from ${reviews} where ${and(eq(reviews.id, write.reviewId), eq(reviews.siteId, site), eq(reviews.status, "pending"))})
|
|
1139
|
+
returning id
|
|
1140
|
+
`)).length === 0) return { outcome: await findReview(db, site, write.reviewId) === null ? "not_found" : "not_pending" };
|
|
1141
|
+
const [row] = await db.select(COMMENT_COLUMNS).from(reviewComments).innerJoin(users, eq(users.id, reviewComments.authorId)).where(and(eq(reviewComments.siteId, site), eq(reviewComments.id, id))).limit(1);
|
|
1142
|
+
if (row === void 0) throw new Error(`The comment ${id} that was just written is not there`);
|
|
1143
|
+
return {
|
|
1144
|
+
outcome: "commented",
|
|
1145
|
+
comment: toComment(row)
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
async function reviewCommentsOf(db, site, reviewIds) {
|
|
1149
|
+
const byReview = /* @__PURE__ */ new Map();
|
|
1150
|
+
if (reviewIds.length === 0) return byReview;
|
|
1151
|
+
const rows = await db.select(COMMENT_COLUMNS).from(reviewComments).innerJoin(users, eq(users.id, reviewComments.authorId)).where(and(eq(reviewComments.siteId, site), inArray(reviewComments.reviewId, [...reviewIds]))).orderBy(asc(reviewComments.createdAt), asc(reviewComments.id));
|
|
1152
|
+
for (const row of rows) {
|
|
1153
|
+
const list = byReview.get(row.reviewId) ?? [];
|
|
1154
|
+
list.push(toComment(row));
|
|
1155
|
+
byReview.set(row.reviewId, list);
|
|
1156
|
+
}
|
|
1157
|
+
return byReview;
|
|
1158
|
+
}
|
|
1106
1159
|
//#endregion
|
|
1107
1160
|
//#region src/db/size.ts
|
|
1108
1161
|
/**
|
|
@@ -1511,10 +1564,12 @@ function createD1Store(db, options = {}) {
|
|
|
1511
1564
|
dueSchedules: (site, now) => dueSchedules(database, site, now),
|
|
1512
1565
|
cancelSchedule: (site, entryId) => cancelSchedule(database, clock, site, entryId),
|
|
1513
1566
|
unpublish: (site, write) => unpublish$1(database, clock, site, write),
|
|
1514
|
-
requestReview: (site, write) => requestReview(database, clock, site, write, () => whyNothingChanged(database, site, write.entryId)),
|
|
1567
|
+
requestReview: (site, write) => requestReview$1(database, clock, site, write, () => whyNothingChanged(database, site, write.entryId)),
|
|
1515
1568
|
findReview: (site, id) => findReview(database, site, id),
|
|
1516
1569
|
listReviews: (site, query) => listReviews$1(database, site, query),
|
|
1517
|
-
requestChanges: (site, write) => requestChanges$1(database, clock, site, write)
|
|
1570
|
+
requestChanges: (site, write) => requestChanges$1(database, clock, site, write),
|
|
1571
|
+
commentReview: (site, write) => commentReview$1(database, clock, site, write),
|
|
1572
|
+
reviewComments: (site, reviewIds) => reviewCommentsOf(database, site, reviewIds)
|
|
1518
1573
|
};
|
|
1519
1574
|
}
|
|
1520
1575
|
//#endregion
|
|
@@ -2485,6 +2540,17 @@ function analyticsReaderOf(env) {
|
|
|
2485
2540
|
});
|
|
2486
2541
|
}
|
|
2487
2542
|
//#endregion
|
|
2543
|
+
//#region src/ops/invoke.ts
|
|
2544
|
+
/**
|
|
2545
|
+
* Calls the handler of an operation. This is the one place where the typed pairing of specs and
|
|
2546
|
+
* handlers meets a dynamic caller (MCP, the ops API): the caller parsed `input` with the input
|
|
2547
|
+
* schema of `spec`, which is the schema that types the handler of the same name.
|
|
2548
|
+
*/
|
|
2549
|
+
function invoke(handlers, spec, input, context) {
|
|
2550
|
+
const handler = handlers[spec.name];
|
|
2551
|
+
return handler(input, context);
|
|
2552
|
+
}
|
|
2553
|
+
//#endregion
|
|
2488
2554
|
//#region src/http/ops-api.ts
|
|
2489
2555
|
const STATUS$1 = {
|
|
2490
2556
|
invalid_input: 400,
|
|
@@ -2499,10 +2565,6 @@ function json(body, status = 200) {
|
|
|
2499
2565
|
});
|
|
2500
2566
|
}
|
|
2501
2567
|
/** Calls the handler of an operation with input its spec has parsed. */
|
|
2502
|
-
function invoke$1(handlers, spec, input, context) {
|
|
2503
|
-
const handler = handlers[spec.name];
|
|
2504
|
-
return handler(input, context);
|
|
2505
|
-
}
|
|
2506
2568
|
function installOpsApi(app, options) {
|
|
2507
2569
|
app.post("/v1/ops/:name", async (c) => options.protect(c, async (principal) => {
|
|
2508
2570
|
const requestId = c.get("requestId");
|
|
@@ -2539,7 +2601,7 @@ function installOpsApi(app, options) {
|
|
|
2539
2601
|
analytics: analyticsReaderOf(c.env)
|
|
2540
2602
|
};
|
|
2541
2603
|
try {
|
|
2542
|
-
return json(spec.output.parse(await invoke
|
|
2604
|
+
return json(spec.output.parse(await invoke(options.handlers, spec, parsed.data, context)));
|
|
2543
2605
|
} catch (error) {
|
|
2544
2606
|
if (error instanceof HollwareError) return json({
|
|
2545
2607
|
code: error.code,
|
|
@@ -2996,7 +3058,7 @@ function plainPageHeaders() {
|
|
|
2996
3058
|
* The ceremonies, with SimpleWebAuthn and its parsers, in a chunk of their own: only these four
|
|
2997
3059
|
* routes use them, and a Worker that serves MCP never pays for them at startup (`budgets.json`).
|
|
2998
3060
|
*/
|
|
2999
|
-
const ceremonies = () => import("./ceremonies-
|
|
3061
|
+
const ceremonies = () => import("./ceremonies-6mZ3u32c.mjs");
|
|
3000
3062
|
/** The cookie of a signed-in person: only `/authorize` reads it (ADR-026). */
|
|
3001
3063
|
const SESSION_COOKIE = "hollware_session";
|
|
3002
3064
|
function sessionCookie(secret) {
|
|
@@ -3843,6 +3905,7 @@ const encoder = new TextEncoder();
|
|
|
3843
3905
|
*/
|
|
3844
3906
|
const OUTPUT_TOO_LARGE_FIX = {
|
|
3845
3907
|
approve_review: "The operation ran and its change may have been applied: read the review with list_reviews before you repeat it.",
|
|
3908
|
+
comment_review: "The operation ran and its comment may have been added: read the review with list_reviews before you repeat it.",
|
|
3846
3909
|
content_stats: "Ask for less: pass a smaller limit.",
|
|
3847
3910
|
create_draft: "The operation ran and its change may have been applied: read the entry with get_entry before you repeat it.",
|
|
3848
3911
|
describe_site: "The description of the site does not fit in one answer. Document fewer collections, components and contracts, or shorten their descriptions.",
|
|
@@ -3852,6 +3915,7 @@ const OUTPUT_TOO_LARGE_FIX = {
|
|
|
3852
3915
|
list_reviews: "Ask for less: pass a smaller limit.",
|
|
3853
3916
|
publish: "The operation ran and its change may have been applied: read the entry with get_entry before you repeat it.",
|
|
3854
3917
|
request_changes: "The operation ran and its change may have been applied: read the review with list_reviews before you repeat it.",
|
|
3918
|
+
request_review: "The operation ran and its review may have been asked: read it with list_reviews before you repeat it.",
|
|
3855
3919
|
search: "Ask for less: pass a smaller limit.",
|
|
3856
3920
|
unpublish: "The operation ran and its change may have been applied: read the entry with get_entry before you repeat it.",
|
|
3857
3921
|
update_draft: "The operation ran and its change may have been applied: read the entry with get_entry before you repeat it.",
|
|
@@ -4047,15 +4111,6 @@ function authInfoOf(principal, resourceMetadataUrl) {
|
|
|
4047
4111
|
};
|
|
4048
4112
|
}
|
|
4049
4113
|
/**
|
|
4050
|
-
* Calls the handler of an operation. This is the one place where the typed pairing of specs and
|
|
4051
|
-
* handlers meets the dynamic protocol: the SDK parsed `input` with the input schema of `spec`,
|
|
4052
|
-
* which is the schema that types the handler of the same name.
|
|
4053
|
-
*/
|
|
4054
|
-
function invoke(handlers, spec, input, context) {
|
|
4055
|
-
const handler = handlers[spec.name];
|
|
4056
|
-
return handler(input, context);
|
|
4057
|
-
}
|
|
4058
|
-
/**
|
|
4059
4114
|
* The MCP endpoint of Hollware: one tool for each operation, served in both protocol eras, for
|
|
4060
4115
|
* the gateway to mount at `/mcp` after it has checked the Host, the Origin and the token.
|
|
4061
4116
|
*
|
|
@@ -4434,6 +4489,10 @@ async function runDueSchedules(input) {
|
|
|
4434
4489
|
//#endregion
|
|
4435
4490
|
//#region src/compose.ts
|
|
4436
4491
|
const systemClock = { now: () => /* @__PURE__ */ new Date() };
|
|
4492
|
+
/** Whether the run of the cron at `time` wakes the agents: on the minutes that divide by ten. */
|
|
4493
|
+
function wakesAgents(time) {
|
|
4494
|
+
return Math.floor(time / 6e4) % 10 === 0;
|
|
4495
|
+
}
|
|
4437
4496
|
/** What the MCP endpoint calls the server, in `server/discover` and `initialize`. */
|
|
4438
4497
|
const SERVER_INFO = {
|
|
4439
4498
|
name: "hollware",
|
|
@@ -4498,7 +4557,7 @@ function createHollwareWith(config, handlers, options = {}) {
|
|
|
4498
4557
|
auth: resolved.auth,
|
|
4499
4558
|
handlers
|
|
4500
4559
|
}),
|
|
4501
|
-
scheduled: async (
|
|
4560
|
+
scheduled: async (controller, env, ctx) => {
|
|
4502
4561
|
const each = async (site) => {
|
|
4503
4562
|
await runDueSchedules({
|
|
4504
4563
|
site,
|
|
@@ -4524,6 +4583,14 @@ function createHollwareWith(config, handlers, options = {}) {
|
|
|
4524
4583
|
});
|
|
4525
4584
|
}
|
|
4526
4585
|
};
|
|
4586
|
+
const agents = env.AGENTS;
|
|
4587
|
+
const run = typeof agents === "object" && agents !== null ? Reflect.get(agents, "run") : void 0;
|
|
4588
|
+
if (typeof run === "function" && wakesAgents(controller.scheduledTime)) ctx.waitUntil(Promise.resolve(Reflect.apply(run, agents, [])).catch((error) => {
|
|
4589
|
+
writeLog({
|
|
4590
|
+
kind: "agents_failed",
|
|
4591
|
+
message: String(error)
|
|
4592
|
+
});
|
|
4593
|
+
}));
|
|
4527
4594
|
ctx.waitUntil((async () => {
|
|
4528
4595
|
for (const site of sites.values()) await each(site).catch((error) => {
|
|
4529
4596
|
writeLog({
|
|
@@ -4649,6 +4716,18 @@ function publicationView(publication) {
|
|
|
4649
4716
|
at: publication.createdAt
|
|
4650
4717
|
};
|
|
4651
4718
|
}
|
|
4719
|
+
/** A comment on a review as the operations answer it; `client` is the name its client gave. */
|
|
4720
|
+
function reviewCommentView(comment) {
|
|
4721
|
+
const name = comment.authorMeta?.["name"];
|
|
4722
|
+
return {
|
|
4723
|
+
id: comment.id,
|
|
4724
|
+
author: comment.authorId,
|
|
4725
|
+
authorName: comment.authorName,
|
|
4726
|
+
client: typeof name === "string" ? name : null,
|
|
4727
|
+
body: comment.body,
|
|
4728
|
+
at: comment.createdAt
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4652
4731
|
//#endregion
|
|
4653
4732
|
//#region src/ops/content-stats.ts
|
|
4654
4733
|
/** A path without its trailing slash, so that `/blog/a` and `/blog/a/` are one page. */
|
|
@@ -5409,13 +5488,15 @@ const listReviews = async (input, context) => {
|
|
|
5409
5488
|
status: input.status,
|
|
5410
5489
|
limit: input.limit
|
|
5411
5490
|
});
|
|
5491
|
+
const comments = await context.store.reviewComments(context.site.key, found.map((review) => review.id));
|
|
5412
5492
|
const reviews = [];
|
|
5413
5493
|
for (const review of found) {
|
|
5414
5494
|
const view = await context.store.findEntryById(context.site.key, review.entryId, "head");
|
|
5415
5495
|
if (view === null) throw unexpectedAnswer(context, "list_reviews", { outcome: "no entry" });
|
|
5416
5496
|
reviews.push({
|
|
5417
5497
|
...reviewView(review),
|
|
5418
|
-
entry: entrySummary(view)
|
|
5498
|
+
entry: entrySummary(view),
|
|
5499
|
+
comments: (comments.get(review.id) ?? []).map(reviewCommentView)
|
|
5419
5500
|
});
|
|
5420
5501
|
}
|
|
5421
5502
|
return {
|
|
@@ -5481,6 +5562,56 @@ const requestChanges = async (input, context) => {
|
|
|
5481
5562
|
review: reviewView(closed.review)
|
|
5482
5563
|
};
|
|
5483
5564
|
};
|
|
5565
|
+
/**
|
|
5566
|
+
* A comment on a pending review, in the name of the caller: for the person who decides and for
|
|
5567
|
+
* the author. It decides nothing; a review that is no longer pending takes none.
|
|
5568
|
+
*/
|
|
5569
|
+
const commentReview = async (input, context) => {
|
|
5570
|
+
const answer = await context.store.commentReview(context.site.key, {
|
|
5571
|
+
reviewId: input.id,
|
|
5572
|
+
actor: actorOf(context),
|
|
5573
|
+
body: input.body
|
|
5574
|
+
});
|
|
5575
|
+
return answer.outcome === "commented" ? {
|
|
5576
|
+
outcome: "commented",
|
|
5577
|
+
comment: reviewCommentView(answer.comment)
|
|
5578
|
+
} : { outcome: answer.outcome };
|
|
5579
|
+
};
|
|
5580
|
+
/**
|
|
5581
|
+
* Asks for a review of the head revision of an entry through `publish`, with `content:publish`
|
|
5582
|
+
* taken from the caller for this call: the same checks run, and the one thing that cannot happen
|
|
5583
|
+
* is a publication. It is how an agent sends its work to a person, whatever its token holds.
|
|
5584
|
+
*/
|
|
5585
|
+
const requestReview = async (input, context) => {
|
|
5586
|
+
const { principal: given } = context;
|
|
5587
|
+
const principal = given.kind === "binding" ? given : {
|
|
5588
|
+
...given,
|
|
5589
|
+
scopes: given.scopes.filter((scope) => scope !== "content:publish")
|
|
5590
|
+
};
|
|
5591
|
+
const answer = await publish$1({
|
|
5592
|
+
id: input.id,
|
|
5593
|
+
version: input.version,
|
|
5594
|
+
acknowledgeWarnings: input.acknowledgeWarnings,
|
|
5595
|
+
group: []
|
|
5596
|
+
}, {
|
|
5597
|
+
...context,
|
|
5598
|
+
principal
|
|
5599
|
+
});
|
|
5600
|
+
switch (answer.outcome) {
|
|
5601
|
+
case "pending_review":
|
|
5602
|
+
case "blocked":
|
|
5603
|
+
case "needs_acknowledgement":
|
|
5604
|
+
case "conflict":
|
|
5605
|
+
case "not_found": return {
|
|
5606
|
+
outcome: answer.outcome,
|
|
5607
|
+
...answer.entry === void 0 ? {} : { entry: answer.entry },
|
|
5608
|
+
...answer.verdict === void 0 ? {} : { verdict: answer.verdict },
|
|
5609
|
+
...answer.conflict === void 0 ? {} : { conflict: answer.conflict },
|
|
5610
|
+
...answer.review === void 0 ? {} : { review: answer.review }
|
|
5611
|
+
};
|
|
5612
|
+
default: throw unexpectedAnswer(context, "request_review", answer);
|
|
5613
|
+
}
|
|
5614
|
+
};
|
|
5484
5615
|
//#endregion
|
|
5485
5616
|
//#region src/ops/unpublish.ts
|
|
5486
5617
|
/**
|
|
@@ -5626,6 +5757,7 @@ function defineHandlers(handlers) {
|
|
|
5626
5757
|
*/
|
|
5627
5758
|
const handlers = defineHandlers({
|
|
5628
5759
|
approve_review: approveReview,
|
|
5760
|
+
comment_review: commentReview,
|
|
5629
5761
|
content_stats: contentStats,
|
|
5630
5762
|
create_draft: createDraft,
|
|
5631
5763
|
describe_site: describeSite$1,
|
|
@@ -5636,6 +5768,7 @@ const handlers = defineHandlers({
|
|
|
5636
5768
|
list_reviews: listReviews,
|
|
5637
5769
|
publish: publish$1,
|
|
5638
5770
|
request_changes: requestChanges,
|
|
5771
|
+
request_review: requestReview,
|
|
5639
5772
|
unpublish,
|
|
5640
5773
|
update_draft: updateDraft,
|
|
5641
5774
|
validate
|
|
@@ -599,6 +599,39 @@ const entryEmbeddings = sqliteTable("entry_embeddings", {
|
|
|
599
599
|
}),
|
|
600
600
|
check("entry_embeddings_pieces", sql`${t.pieces} >= 1`)
|
|
601
601
|
]);
|
|
602
|
+
/**
|
|
603
|
+
* Comments on a review (v0.6): what a person or an agent says about the revision a review asks
|
|
604
|
+
* for, before someone decides. Only a pending review takes one; they stay after the decision.
|
|
605
|
+
*/
|
|
606
|
+
const reviewComments = sqliteTable("review_comments", {
|
|
607
|
+
id: id("rvc"),
|
|
608
|
+
siteId: siteId(),
|
|
609
|
+
reviewId: text("review_id").notNull(),
|
|
610
|
+
authorId: text("author_id").notNull(),
|
|
611
|
+
/** What the client that wrote it said about itself, as JSON, or `null`. */
|
|
612
|
+
authorMeta: text("author_meta"),
|
|
613
|
+
body: text("body").notNull(),
|
|
614
|
+
...timestamps()
|
|
615
|
+
}, (t) => [
|
|
616
|
+
unique("review_comments_id_site").on(t.id, t.siteId),
|
|
617
|
+
index("review_comments_review").on(t.siteId, t.reviewId, t.createdAt),
|
|
618
|
+
foreignKey({
|
|
619
|
+
name: "review_comments_site",
|
|
620
|
+
columns: [t.siteId],
|
|
621
|
+
foreignColumns: [sites.id]
|
|
622
|
+
}),
|
|
623
|
+
foreignKey({
|
|
624
|
+
name: "review_comments_review_fk",
|
|
625
|
+
columns: [t.reviewId, t.siteId],
|
|
626
|
+
foreignColumns: [reviews.id, reviews.siteId]
|
|
627
|
+
}),
|
|
628
|
+
foreignKey({
|
|
629
|
+
name: "review_comments_author",
|
|
630
|
+
columns: [t.authorId],
|
|
631
|
+
foreignColumns: [users.id]
|
|
632
|
+
}),
|
|
633
|
+
check("review_comments_body", sql`length(${t.body}) BETWEEN 1 AND 4000`)
|
|
634
|
+
]);
|
|
602
635
|
//#endregion
|
|
603
636
|
//#region src/identity/secrets.ts
|
|
604
637
|
/**
|
|
@@ -798,4 +831,4 @@ async function invitationById(db, id) {
|
|
|
798
831
|
};
|
|
799
832
|
}
|
|
800
833
|
//#endregion
|
|
801
|
-
export {
|
|
834
|
+
export { users as A, reviewComments as C, schedules as D, revisions as E, effectiveScopes as M, isRole as N, sites as O, publications as S, revisionTags as T, entries as _, findInvitation as a, members as b, invitationById as c, redeemInvitation as d, saveChallenge as f, deliveries as g, userHandle as h, createUser as i, ROLE_SCOPES as j, tokens as k, membershipsOf as l, touchPasskey as m, createInvitation as n, findPasskey as o, takeChallenge as p, createSession as r, findSession as s, addPasskey as t, passkeysOf as u, entryEmbeddings as v, reviews as w, outbox as x, invitations as y };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE `review_comments` (
|
|
2
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`site_id` text NOT NULL,
|
|
4
|
+
`review_id` text NOT NULL,
|
|
5
|
+
`author_id` text NOT NULL,
|
|
6
|
+
`author_meta` text,
|
|
7
|
+
`body` text NOT NULL,
|
|
8
|
+
`created_at` text NOT NULL,
|
|
9
|
+
`updated_at` text NOT NULL,
|
|
10
|
+
FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON UPDATE no action ON DELETE no action,
|
|
11
|
+
FOREIGN KEY (`review_id`,`site_id`) REFERENCES `reviews`(`id`,`site_id`) ON UPDATE no action ON DELETE no action,
|
|
12
|
+
FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
|
13
|
+
CONSTRAINT "review_comments_body" CHECK(length("review_comments"."body") BETWEEN 1 AND 4000)
|
|
14
|
+
);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE INDEX `review_comments_review` ON `review_comments` (`site_id`,`review_id`,`created_at`);--> statement-breakpoint
|
|
17
|
+
CREATE UNIQUE INDEX `review_comments_id_site` ON `review_comments` (`id`,`site_id`);
|