@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.
@@ -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-DiS8UFI2.mjs";
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 ROLE_SCOPES, C as reviews, D as sites, E as schedules, M as isRole, O as tokens, S as publications, T as revisions, _ as entries, b as members, g as deliveries, j as effectiveScopes, k as users, l as membershipsOf, n as createInvitation, s as findSession, v as entryEmbeddings, w as revisionTags, x as outbox, y as invitations } from "./store-DiS8UFI2.mjs";
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.5.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$1(options.handlers, spec, parsed.data, context)));
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-BM9G03lZ.mjs");
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 (_controller, env, ctx) => {
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 { ROLE_SCOPES as A, reviews as C, sites as D, schedules as E, isRole as M, tokens as O, publications as S, revisions 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, effectiveScopes as j, users 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, revisionTags as w, outbox as x, invitations as y };
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`);