@happyvertical/smrt-comments 0.57.1 → 0.57.3

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.
Files changed (50) hide show
  1. package/README.md +72 -0
  2. package/dist/chunks/participation-LA_cfrM2.js +276 -0
  3. package/dist/chunks/participation-LA_cfrM2.js.map +1 -0
  4. package/dist/collections/CommentReportCollection.d.ts +6 -0
  5. package/dist/collections/CommentReportCollection.d.ts.map +1 -0
  6. package/dist/collections/CommentRevisionCollection.d.ts +6 -0
  7. package/dist/collections/CommentRevisionCollection.d.ts.map +1 -0
  8. package/dist/collections/ParticipationCollection.d.ts +14 -0
  9. package/dist/collections/ParticipationCollection.d.ts.map +1 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +364 -59
  13. package/dist/index.js.map +1 -1
  14. package/dist/manifest.json +1853 -224
  15. package/dist/models/Comment.d.ts +8 -1
  16. package/dist/models/Comment.d.ts.map +1 -1
  17. package/dist/models/CommentReport.d.ts +10 -0
  18. package/dist/models/CommentReport.d.ts.map +1 -0
  19. package/dist/models/CommentRevision.d.ts +9 -0
  20. package/dist/models/CommentRevision.d.ts.map +1 -0
  21. package/dist/models/ParticipationBan.d.ts +12 -0
  22. package/dist/models/ParticipationBan.d.ts.map +1 -0
  23. package/dist/models/ParticipationPolicy.d.ts +13 -0
  24. package/dist/models/ParticipationPolicy.d.ts.map +1 -0
  25. package/dist/models/ParticipationRateBucket.d.ts +8 -0
  26. package/dist/models/ParticipationRateBucket.d.ts.map +1 -0
  27. package/dist/participation-contract.d.ts +37 -0
  28. package/dist/participation-contract.d.ts.map +1 -0
  29. package/dist/participation.d.ts +4 -0
  30. package/dist/participation.d.ts.map +1 -0
  31. package/dist/participation.js +2 -0
  32. package/dist/recipes.d.ts +6 -1
  33. package/dist/recipes.d.ts.map +1 -1
  34. package/dist/services/CommentService.d.ts +10 -1
  35. package/dist/services/CommentService.d.ts.map +1 -1
  36. package/dist/services/ParticipationService.d.ts +25 -0
  37. package/dist/services/ParticipationService.d.ts.map +1 -0
  38. package/dist/smrt-knowledge.json +477 -12
  39. package/dist/svelte/ModerationQueue.svelte +60 -0
  40. package/dist/svelte/ModerationQueue.svelte.d.ts +17 -0
  41. package/dist/svelte/ModerationQueue.svelte.d.ts.map +1 -0
  42. package/dist/svelte/RecordComments.svelte +21 -6
  43. package/dist/svelte/RecordComments.svelte.d.ts +2 -0
  44. package/dist/svelte/RecordComments.svelte.d.ts.map +1 -1
  45. package/dist/svelte/index.d.ts +2 -0
  46. package/dist/svelte/index.d.ts.map +1 -1
  47. package/dist/svelte/index.js +1 -0
  48. package/dist/types.d.ts +7 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +9 -5
package/dist/index.js CHANGED
@@ -1,24 +1,22 @@
1
- import { ObjectRegistry, SmrtCollection, SmrtPolymorphicAssociation, SmrtRecipe, crossPackageRef, digestRunOnceContent, field, runOnce, smrt } from "@happyvertical/smrt-core";
1
+ import { a as ParticipationRateBucketCollection, c as ParticipationBan, i as ParticipationPolicyCollection, n as ParticipationDeniedError, o as ParticipationRateBucket, r as ParticipationBanCollection, s as ParticipationPolicy, t as ParticipationService } from "./chunks/participation-LA_cfrM2.js";
2
+ import { SmrtCollection, SmrtObject, SmrtPolymorphicAssociation, SmrtRecipe, crossPackageRef, digestRunOnceContent, field, foreignKey, runOnce, smrt } from "@happyvertical/smrt-core";
2
3
  import { TenantIsolationError, TenantScoped, getCurrentTenant, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
3
- //#region src/__smrt-register__.ts
4
- ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-comments","packageVersion":"0.57.1","objects":{"@happyvertical/smrt-comments:CommentCollection":{"name":"commentcollection","className":"CommentCollection","qualifiedName":"@happyvertical/smrt-comments:CommentCollection","collection":"comments","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/CommentCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{"listForRecord":{"name":"listForRecord","async":true,"parameters":[{"name":"recordType","type":"string","optional":false},{"name":"recordId","type":"string","optional":false}],"returnType":"Promise<Comment[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"record_comments","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"Comment","exportName":"CommentCollection","collectionExportName":"CommentCollectionCollection","schema":{"tableName":"record_comments","ddl":"CREATE TABLE IF NOT EXISTS \\"record_comments\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"meta_type\\" TEXT NOT NULL,\\n \\"meta_id\\" TEXT NOT NULL,\\n \\"role\\" TEXT NOT NULL,\\n \\"sort_order\\" INTEGER DEFAULT 0,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"author_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL,\\n \\"mentions\\" TEXT,\\n \\"idempotency_key\\" TEXT,\\n \\"retry_tenant_id\\" TEXT,\\n \\"request_digest\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"meta_type":{"type":"TEXT","notNull":true,"unique":false},"meta_id":{"type":"TEXT","notNull":true,"unique":false},"role":{"type":"TEXT","notNull":true,"unique":false},"sort_order":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"author_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false},"mentions":{"type":"TEXT","notNull":false,"unique":false},"idempotency_key":{"type":"TEXT","notNull":false,"unique":false},"retry_tenant_id":{"type":"TEXT","notNull":false,"unique":false},"request_digest":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"record_comments_tenant_request_key","columns":["retry_tenant_id","idempotency_key"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenant_id","meta_type","meta_id","created_at"]},{"name":"record_comments_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"record_comments_meta_type_meta_id_idx","columns":["meta_type","meta_id"]},{"name":"record_comments_author_user_id_idx","columns":["author_user_id"]}],"version":"5095dfc9"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:Comment":{"name":"comment","className":"Comment","qualifiedName":"@happyvertical/smrt-comments:Comment","collection":"comments","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/Comment.ts","packageName":"@happyvertical/smrt-comments","fields":{"metaType":{"type":"text","required":true,"_meta":{"required":true}},"metaId":{"type":"text","required":true,"_meta":{"required":true}},"role":{"type":"text","required":true,"_meta":{"required":true}},"sortOrder":{"type":"integer","required":false,"default":0},"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"authorUserId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:User"},"body":{"type":"text","required":true,"_meta":{"required":true}},"mentions":{"type":"text","required":false},"idempotencyKey":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"retryTenantId":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"requestDigest":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true}},"methods":{"hydrate":{"name":"hydrate","async":true,"parameters":[],"returnType":"Promise<T | null>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"record_comments","api":false,"mcp":false,"cli":false,"indexes":[{"name":"record_comments_tenant_request_key","columns":["retryTenantId","idempotencyKey"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenantId","metaType","metaId","createdAt"]}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtPolymorphicAssociation","exportName":"Comment","collectionExportName":"CommentCollection","validationRules":[{"field":"metaType","rule":"required","fieldType":"text"},{"field":"metaId","rule":"required","fieldType":"text"},{"field":"role","rule":"required","fieldType":"text"},{"field":"authorUserId","rule":"required","fieldType":"crossPackageRef"},{"field":"body","rule":"required","fieldType":"text"}],"schema":{"tableName":"record_comments","ddl":"CREATE TABLE IF NOT EXISTS \\"record_comments\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"meta_type\\" TEXT NOT NULL,\\n \\"meta_id\\" TEXT NOT NULL,\\n \\"role\\" TEXT NOT NULL,\\n \\"sort_order\\" INTEGER DEFAULT 0,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"author_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL,\\n \\"mentions\\" TEXT,\\n \\"idempotency_key\\" TEXT,\\n \\"retry_tenant_id\\" TEXT,\\n \\"request_digest\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"meta_type":{"type":"TEXT","notNull":true,"unique":false},"meta_id":{"type":"TEXT","notNull":true,"unique":false},"role":{"type":"TEXT","notNull":true,"unique":false},"sort_order":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"author_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false},"mentions":{"type":"TEXT","notNull":false,"unique":false},"idempotency_key":{"type":"TEXT","notNull":false,"unique":false},"retry_tenant_id":{"type":"TEXT","notNull":false,"unique":false},"request_digest":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"record_comments_tenant_request_key","columns":["retry_tenant_id","idempotency_key"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenant_id","meta_type","meta_id","created_at"]},{"name":"record_comments_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"record_comments_meta_type_meta_id_idx","columns":["meta_type","meta_id"]},{"name":"record_comments_author_user_id_idx","columns":["author_user_id"]}],"version":"5095dfc9"},"importPath":"@happyvertical/smrt-comments"}},"moduleType":"smrt","recipes":[{"id":"comments.records","className":"RecordCommentsRecipe","label":"Record comments","summary":"Discuss work directly on the records it concerns.","synonyms":["comments","mentions","discussion"],"models":["@happyvertical/smrt-comments:Comment"],"nav":[],"requires":[],"group":{"id":"comments","label":"Comments","summary":"Keep discussions alongside the records your team works on."},"help":{"markdown":"## Overview\\n\\nRecord comments keep a discussion next to the work it concerns. Open a record\\nyou can access to read its discussion or add a comment. Comments are part of\\nthe record page, rather than a separate list of every discussion.\\n\\n## Tasks\\n\\n### Add a comment\\n\\nOpen the record, find Comments, write your message and choose Post comment.\\nYour app controls who can read the record and who can contribute to its discussion.\\n\\n### Mention someone\\n\\nIf your app provides mention selection, choose a teammate who can access the\\nrecord. When mention notifications are connected, that person receives a\\nnotification. Typing a name alone does not select a recipient.\\n\\n### Check a failed post\\n\\nIf posting reports an error, refresh the discussion before trying again. A\\ncomment may have been saved even when its mention notification could not be\\ndelivered. If it appears in the discussion, do not post it a second time.\\n","fieldRefs":[]},"surfaces":[{"kind":"playground","export":"@happyvertical/smrt-comments/svelte#RecordCommentsPlayground","label":"Record discussion preview"}],"runtime":"both","demoSeed":{"export":"@happyvertical/smrt-comments/svelte#recordCommentsDemo"},"demo":{"mode":"server","reasons":["Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."]}}],"smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-messages","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3624"],"via":["@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
5
- //#endregion
6
4
  //#region src/models/Comment.ts
7
- var __defProp = Object.defineProperty;
8
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
5
+ var __defProp$2 = Object.defineProperty;
6
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
7
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, {
10
8
  enumerable: true,
11
9
  configurable: true,
12
10
  writable: true,
13
11
  value
14
12
  }) : obj[key] = value;
15
- var __decorateClass = (decorators, target, key, kind) => {
16
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
13
+ var __decorateClass$2 = (decorators, target, key, kind) => {
14
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
17
15
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
18
- if (kind && result) __defProp(target, key, result);
16
+ if (kind && result) __defProp$2(target, key, result);
19
17
  return result;
20
18
  };
21
- var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
19
+ var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
22
20
  var MAX_MENTIONS = 50;
23
21
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
22
  var Comment = class extends SmrtPolymorphicAssociation {
@@ -30,6 +28,10 @@ var Comment = class extends SmrtPolymorphicAssociation {
30
28
  idempotencyKey = null;
31
29
  retryTenantId = null;
32
30
  requestDigest = null;
31
+ parentCommentId = null;
32
+ status = "visible";
33
+ participationSnapshot = null;
34
+ editedAt = null;
33
35
  constructor(options = {}) {
34
36
  super(options);
35
37
  if (options.tenantId !== void 0) this.tenantId = options.tenantId;
@@ -38,6 +40,9 @@ var Comment = class extends SmrtPolymorphicAssociation {
38
40
  if (options.idempotencyKey !== void 0) this.idempotencyKey = options.idempotencyKey;
39
41
  if (options.retryTenantId !== void 0) this.retryTenantId = options.retryTenantId;
40
42
  if (options.requestDigest !== void 0) this.requestDigest = options.requestDigest;
43
+ if (options.parentCommentId !== void 0) this.parentCommentId = options.parentCommentId;
44
+ if (options.status !== void 0) this.status = options.status;
45
+ if (options.participationSnapshot !== void 0) this.participationSnapshot = options.participationSnapshot;
41
46
  const mentions = options.mentions;
42
47
  if (Array.isArray(mentions)) this.mentionUserIds = mentions;
43
48
  else if (typeof mentions === "string") this.mentions = mentions;
@@ -56,43 +61,77 @@ var Comment = class extends SmrtPolymorphicAssociation {
56
61
  this.mentions = JSON.stringify(unique);
57
62
  }
58
63
  };
59
- __publicField(Comment, "__smrtPackage__", "@happyvertical/smrt-comments");
60
- __decorateClass([tenantId()], Comment.prototype, "tenantId", 2);
61
- __decorateClass([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Comment.prototype, "authorUserId", 2);
62
- __decorateClass([field({ required: true })], Comment.prototype, "body", 2);
63
- __decorateClass([field({
64
+ __publicField$2(Comment, "__smrtPackage__", "@happyvertical/smrt-comments");
65
+ __decorateClass$2([tenantId()], Comment.prototype, "tenantId", 2);
66
+ __decorateClass$2([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Comment.prototype, "authorUserId", 2);
67
+ __decorateClass$2([field({ required: true })], Comment.prototype, "body", 2);
68
+ __decorateClass$2([field({
64
69
  type: "text",
65
70
  nullable: true,
66
71
  readonly: true
67
72
  })], Comment.prototype, "idempotencyKey", 2);
68
- __decorateClass([field({
73
+ __decorateClass$2([field({
69
74
  type: "text",
70
75
  nullable: true,
71
76
  readonly: true
72
77
  })], Comment.prototype, "retryTenantId", 2);
73
- __decorateClass([field({
78
+ __decorateClass$2([field({
74
79
  type: "text",
75
80
  nullable: true,
76
81
  readonly: true
77
82
  })], Comment.prototype, "requestDigest", 2);
78
- Comment = __decorateClass([TenantScoped({ mode: "required" }), smrt({
83
+ __decorateClass$2([foreignKey("Comment", {
84
+ nullable: true,
85
+ constraint: false
86
+ })], Comment.prototype, "parentCommentId", 2);
87
+ __decorateClass$2([field({ required: true })], Comment.prototype, "status", 2);
88
+ __decorateClass$2([field({
89
+ type: "text",
90
+ nullable: true,
91
+ readonly: true
92
+ })], Comment.prototype, "participationSnapshot", 2);
93
+ __decorateClass$2([field({
94
+ type: "datetime",
95
+ nullable: true,
96
+ readonly: true
97
+ })], Comment.prototype, "editedAt", 2);
98
+ Comment = __decorateClass$2([TenantScoped({ mode: "required" }), smrt({
79
99
  tableName: "record_comments",
80
100
  api: false,
81
101
  mcp: false,
82
102
  cli: false,
83
- indexes: [{
84
- name: "record_comments_tenant_request_key",
85
- columns: ["retryTenantId", "idempotencyKey"],
86
- unique: true
87
- }, {
88
- name: "record_comments_record_created_idx",
89
- columns: [
90
- "tenantId",
91
- "metaType",
92
- "metaId",
93
- "createdAt"
94
- ]
95
- }]
103
+ indexes: [
104
+ {
105
+ name: "record_comments_tenant_request_key",
106
+ columns: ["retryTenantId", "idempotencyKey"],
107
+ unique: true
108
+ },
109
+ {
110
+ name: "record_comments_record_created_idx",
111
+ columns: [
112
+ "tenantId",
113
+ "metaType",
114
+ "metaId",
115
+ "createdAt"
116
+ ]
117
+ },
118
+ {
119
+ name: "record_comments_parent_created_idx",
120
+ columns: [
121
+ "tenantId",
122
+ "parentCommentId",
123
+ "createdAt"
124
+ ]
125
+ },
126
+ {
127
+ name: "record_comments_moderation_idx",
128
+ columns: [
129
+ "tenantId",
130
+ "status",
131
+ "createdAt"
132
+ ]
133
+ }
134
+ ]
96
135
  })], Comment);
97
136
  //#endregion
98
137
  //#region src/collections/CommentCollection.ts
@@ -112,6 +151,108 @@ var CommentCollection = class extends SmrtCollection {
112
151
  }
113
152
  };
114
153
  //#endregion
154
+ //#region src/models/CommentReport.ts
155
+ var __defProp$1 = Object.defineProperty;
156
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
157
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, {
158
+ enumerable: true,
159
+ configurable: true,
160
+ writable: true,
161
+ value
162
+ }) : obj[key] = value;
163
+ var __decorateClass$1 = (decorators, target, key, kind) => {
164
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
165
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
166
+ if (kind && result) __defProp$1(target, key, result);
167
+ return result;
168
+ };
169
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
170
+ var CommentReport = class extends SmrtObject {
171
+ tenantId = "";
172
+ commentId = "";
173
+ reporterUserId = "";
174
+ reason = "";
175
+ status = "open";
176
+ };
177
+ __publicField$1(CommentReport, "__smrtPackage__", "@happyvertical/smrt-comments");
178
+ __decorateClass$1([tenantId()], CommentReport.prototype, "tenantId", 2);
179
+ __decorateClass$1([foreignKey("Comment", {
180
+ required: true,
181
+ constraint: false
182
+ })], CommentReport.prototype, "commentId", 2);
183
+ __decorateClass$1([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], CommentReport.prototype, "reporterUserId", 2);
184
+ __decorateClass$1([field({ required: true })], CommentReport.prototype, "reason", 2);
185
+ __decorateClass$1([field({ required: true })], CommentReport.prototype, "status", 2);
186
+ CommentReport = __decorateClass$1([TenantScoped({ mode: "required" }), smrt({
187
+ tableName: "comment_reports",
188
+ api: false,
189
+ mcp: false,
190
+ cli: false,
191
+ indexes: [{
192
+ name: "comment_reports_actor_target_unique",
193
+ columns: [
194
+ "tenantId",
195
+ "reporterUserId",
196
+ "commentId"
197
+ ],
198
+ unique: true
199
+ }, {
200
+ name: "comment_reports_queue_idx",
201
+ columns: [
202
+ "tenantId",
203
+ "status",
204
+ "createdAt"
205
+ ]
206
+ }]
207
+ })], CommentReport);
208
+ //#endregion
209
+ //#region src/collections/CommentReportCollection.ts
210
+ var CommentReportCollection = class extends SmrtCollection {
211
+ static _itemClass = CommentReport;
212
+ };
213
+ //#endregion
214
+ //#region src/models/CommentRevision.ts
215
+ var __defProp = Object.defineProperty;
216
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
217
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
218
+ enumerable: true,
219
+ configurable: true,
220
+ writable: true,
221
+ value
222
+ }) : obj[key] = value;
223
+ var __decorateClass = (decorators, target, key, kind) => {
224
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
225
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
226
+ if (kind && result) __defProp(target, key, result);
227
+ return result;
228
+ };
229
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
230
+ var CommentRevision = class extends SmrtObject {
231
+ tenantId = "";
232
+ commentId = "";
233
+ editorUserId = "";
234
+ body = "";
235
+ };
236
+ __publicField(CommentRevision, "__smrtPackage__", "@happyvertical/smrt-comments");
237
+ __decorateClass([tenantId()], CommentRevision.prototype, "tenantId", 2);
238
+ __decorateClass([foreignKey("Comment", {
239
+ required: true,
240
+ constraint: false
241
+ })], CommentRevision.prototype, "commentId", 2);
242
+ __decorateClass([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], CommentRevision.prototype, "editorUserId", 2);
243
+ __decorateClass([field({ required: true })], CommentRevision.prototype, "body", 2);
244
+ CommentRevision = __decorateClass([TenantScoped({ mode: "required" }), smrt({
245
+ tableName: "comment_revisions",
246
+ api: false,
247
+ mcp: false,
248
+ cli: false
249
+ })], CommentRevision);
250
+ //#endregion
251
+ //#region src/collections/CommentRevisionCollection.ts
252
+ var CommentRevisionCollection = class extends SmrtCollection {
253
+ static _itemClass = CommentRevision;
254
+ };
255
+ //#endregion
115
256
  //#region src/recipes.ts
116
257
  var RecordCommentsRecipe = class extends SmrtRecipe {
117
258
  static id = "comments.records";
@@ -127,7 +268,14 @@ var RecordCommentsRecipe = class extends SmrtRecipe {
127
268
  label: "Comments",
128
269
  summary: "Keep discussions alongside the records your team works on."
129
270
  };
130
- static models = [Comment];
271
+ static models = [
272
+ Comment,
273
+ CommentRevision,
274
+ CommentReport,
275
+ ParticipationPolicy,
276
+ ParticipationBan,
277
+ ParticipationRateBucket
278
+ ];
131
279
  static nav = [];
132
280
  static runtime = "both";
133
281
  static surfaces = [{
@@ -143,7 +291,7 @@ var RecordCommentsRecipe = class extends SmrtRecipe {
143
291
  var canonicalUuid = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value.toLowerCase() : value;
144
292
  var CommentIdempotencyError = class extends Error {
145
293
  constructor(code) {
146
- super(code === "COMMENT_REQUEST_CONFLICT" ? "Comment request key already used with different content." : "Comment request no longer has an unchanged comment.");
294
+ super(code === "COMMENT_REQUEST_CONFLICT" ? "Comment request key already used with different content." : "Comment request no longer has a valid retained creation binding.");
147
295
  this.code = code;
148
296
  this.name = "CommentIdempotencyError";
149
297
  }
@@ -163,6 +311,130 @@ var CommentService = class {
163
311
  action
164
312
  })) throw new Error("Comment record access denied.");
165
313
  }
314
+ async getAuthorizedComment(commentId, action) {
315
+ const tenantId = this.options.actor.tenantId;
316
+ const comment = await withTenant({ tenantId }, async () => (await CommentCollection.create({ db: this.options.db })).get(commentId));
317
+ if (!comment || canonicalUuid(comment.tenantId) !== canonicalUuid(tenantId)) throw new Error("Comment not found.");
318
+ await this.authorize(comment.metaType, comment.metaId, action);
319
+ return comment;
320
+ }
321
+ /** Edits retain the exact former body before changing the visible comment. */
322
+ async edit(commentId, body) {
323
+ if (!body.trim()) throw new TypeError("Comment body is required.");
324
+ const comment = await this.getAuthorizedComment(commentId, "read");
325
+ if (canonicalUuid(comment.authorUserId) !== canonicalUuid(this.options.actor.userId)) throw new Error("Comment edit denied.");
326
+ await this.authorize(comment.metaType, comment.metaId, "comment");
327
+ const tenantId = this.options.actor.tenantId;
328
+ return withTenant({ tenantId }, async () => {
329
+ const collection = await CommentCollection.create({ db: this.options.db });
330
+ if (!collection.db.transaction) throw new Error("Comment edits require transactions.");
331
+ return collection.db.transaction(async (tx) => {
332
+ const scoped = await CommentCollection.create({ db: tx });
333
+ const table = `"${scoped.tableName.replace(/"/g, "\"\"")}"`;
334
+ await tx.query(`UPDATE ${table} SET body = body WHERE id = ? AND tenant_id = ?`, commentId, tenantId);
335
+ const current = await scoped.get(commentId);
336
+ if (!current || canonicalUuid(current.authorUserId) !== canonicalUuid(this.options.actor.userId)) throw new Error("Comment edit denied.");
337
+ return new ParticipationService({
338
+ db: tx,
339
+ entitlement: this.options.participation
340
+ }).executeInTransaction(tx, {
341
+ tenantId,
342
+ actorUserId: this.options.actor.userId,
343
+ action: "comment",
344
+ recordType: current.metaType,
345
+ recordId: current.metaId
346
+ }, async (guardedTx, decision) => withTenant({ tenantId: current.tenantId }, async () => {
347
+ await (await CommentRevisionCollection.create({ db: guardedTx })).create({
348
+ tenantId: current.tenantId,
349
+ commentId,
350
+ editorUserId: this.options.actor.userId,
351
+ body: current.body
352
+ });
353
+ if (current.status === "visible" && decision.initialCommentStatus === "pending") current.status = "pending";
354
+ current.body = body;
355
+ current.editedAt = /* @__PURE__ */ new Date();
356
+ await current.save();
357
+ return current;
358
+ }));
359
+ });
360
+ });
361
+ }
362
+ /** A moderator may make a row visible, hidden, pending, or deleted. */
363
+ async moderate(commentId, status) {
364
+ const comment = await this.getAuthorizedComment(commentId, "moderate");
365
+ if (![
366
+ "visible",
367
+ "hidden",
368
+ "pending",
369
+ "deleted"
370
+ ].includes(status)) throw new TypeError("Invalid comment moderation status.");
371
+ return withTenant({ tenantId: comment.tenantId }, async () => {
372
+ const collection = await CommentCollection.create({ db: this.options.db });
373
+ if (!collection.db.transaction) throw new Error("Comment moderation requires transactions.");
374
+ return collection.db.transaction(async (tx) => {
375
+ const scoped = await CommentCollection.create({ db: tx });
376
+ const table = `"${scoped.tableName.replace(/"/g, "\"\"")}"`;
377
+ await tx.query(`UPDATE ${table} SET body = body WHERE id = ? AND tenant_id = ?`, commentId, comment.tenantId);
378
+ const current = await scoped.get(commentId);
379
+ if (!current) throw new Error("Comment not found.");
380
+ current.status = status;
381
+ await current.save();
382
+ return current;
383
+ });
384
+ });
385
+ }
386
+ /** One retained report per actor/comment; new reports consume shared report quota. */
387
+ async report(commentId, reason) {
388
+ if (!reason.trim()) throw new TypeError("Comment report reason is required.");
389
+ const comment = await this.getAuthorizedComment(commentId, "read");
390
+ if (comment.status !== "visible") throw new Error("Comment is not reportable.");
391
+ commentId = canonicalUuid(comment.id ?? commentId);
392
+ const tenantId = canonicalUuid(this.options.actor.tenantId);
393
+ const reporterUserId = canonicalUuid(this.options.actor.userId);
394
+ const request = {
395
+ tenantId,
396
+ actorUserId: reporterUserId,
397
+ action: "report",
398
+ recordType: comment.metaType,
399
+ recordId: comment.metaId
400
+ };
401
+ const guard = new ParticipationService({
402
+ db: this.options.db,
403
+ entitlement: this.options.participation
404
+ });
405
+ await guard.authorize(request);
406
+ return withTenant({ tenantId }, async () => {
407
+ const reports = await CommentReportCollection.create({ db: this.options.db });
408
+ const id = await runOnce({
409
+ db: reports.db,
410
+ tenantId,
411
+ actor: "smrt-comments.report:v1",
412
+ token: `${reporterUserId}:${canonicalUuid(commentId)}`,
413
+ content: "retained-report:v1"
414
+ }, async (tx) => {
415
+ const existing = await (await CommentReportCollection.create({ db: tx })).list({ where: {
416
+ tenantId,
417
+ reporterUserId,
418
+ commentId
419
+ } });
420
+ if (existing[0]) return existing[0].id;
421
+ return guard.executeInTransaction(tx, request, async (guardedTx) => {
422
+ const current = await withTenant({ tenantId: comment.tenantId }, async () => (await CommentCollection.create({ db: guardedTx })).get(commentId));
423
+ if (!current || current.status !== "visible") throw new Error("Comment is not reportable.");
424
+ return (await (await CommentReportCollection.create({ db: guardedTx })).create({
425
+ tenantId,
426
+ commentId,
427
+ reporterUserId,
428
+ reason: reason.trim(),
429
+ status: "open"
430
+ })).id;
431
+ });
432
+ });
433
+ const report = id ? await reports.get(id) : null;
434
+ if (!report || report.commentId !== canonicalUuid(commentId) || report.reporterUserId !== reporterUserId) throw new Error("Retained report unavailable.");
435
+ return report;
436
+ });
437
+ }
166
438
  async listForRecord(recordType, recordId) {
167
439
  await this.authorize(recordType, recordId, "read");
168
440
  const tenantId = this.options.actor.tenantId;
@@ -187,12 +459,24 @@ var CommentService = class {
187
459
  return row;
188
460
  }
189
461
  async create(commentOptions) {
190
- const { tenantId: inputTenantId, authorUserId: inputAuthorUserId, metaType, metaId, body, mentions, idempotencyKey } = commentOptions;
462
+ const { tenantId: inputTenantId, authorUserId: inputAuthorUserId, metaType, metaId, body, mentions, idempotencyKey, parentCommentId: inputParentCommentId } = commentOptions;
463
+ const parentCommentId = inputParentCommentId ? canonicalUuid(inputParentCommentId) : inputParentCommentId;
191
464
  if (!inputTenantId || !inputAuthorUserId) throw new TypeError("Comments require a tenant and author.");
192
465
  if (canonicalUuid(inputTenantId) !== canonicalUuid(this.options.actor.tenantId) || canonicalUuid(inputAuthorUserId) !== canonicalUuid(this.options.actor.userId)) throw new Error("Comment actor context mismatch.");
193
466
  const tenantId = this.options.actor.tenantId;
194
467
  const authorUserId = this.options.actor.userId;
195
468
  await this.authorize(metaType ?? "", metaId ?? "", "comment");
469
+ const participationRequest = {
470
+ tenantId,
471
+ actorUserId: authorUserId,
472
+ action: "comment",
473
+ recordType: metaType ?? "",
474
+ recordId: metaId ?? ""
475
+ };
476
+ await new ParticipationService({
477
+ db: this.options.db,
478
+ entitlement: this.options.participation
479
+ }).authorize(participationRequest);
196
480
  if (!body?.trim()) throw new TypeError("Comment body is required.");
197
481
  const prepared = new Comment();
198
482
  if (mentions !== void 0) prepared.mentionUserIds = mentions;
@@ -204,27 +488,43 @@ var CommentService = class {
204
488
  metaType,
205
489
  metaId,
206
490
  body,
207
- mentions: normalizedMentions
491
+ mentions: normalizedMentions,
492
+ ...parentCommentId ? { parentCommentId } : {}
208
493
  });
209
494
  const comment = await withTenant({ tenantId }, async () => {
210
495
  const collection = await CommentCollection.create({ db: this.options.db });
211
- const insert = async (target) => {
212
- const id = crypto.randomUUID();
213
- return target.create({
214
- tenantId,
215
- authorUserId,
216
- metaType,
217
- metaId,
218
- body,
219
- id,
220
- slug: id,
221
- context: "",
222
- _insertOnly: true,
223
- mentions: prepared.mentions,
224
- idempotencyKey: idempotencyKey ?? null,
225
- requestDigest: idempotencyKey === void 0 ? null : requestDigest,
226
- retryTenantId: idempotencyKey === void 0 ? null : canonicalUuid(tenantId)
496
+ const insert = async (target, inTransaction = false) => {
497
+ const guard = new ParticipationService({
498
+ db: target.db,
499
+ entitlement: this.options.participation
500
+ });
501
+ const work = async (tx, participation) => withTenant({ tenantId }, async () => {
502
+ target = await CommentCollection.create({ db: tx });
503
+ if (parentCommentId) {
504
+ const parent = await target.get(parentCommentId);
505
+ if (!parent || canonicalUuid(parent.tenantId) !== canonicalUuid(tenantId) || parent.metaType !== metaType || parent.metaId !== metaId) throw new Error("Comment parent does not belong to this record.");
506
+ }
507
+ const id = crypto.randomUUID();
508
+ return target.create({
509
+ tenantId,
510
+ authorUserId,
511
+ metaType,
512
+ metaId,
513
+ body,
514
+ id,
515
+ slug: id,
516
+ context: "",
517
+ _insertOnly: true,
518
+ mentions: prepared.mentions,
519
+ idempotencyKey: idempotencyKey ?? null,
520
+ requestDigest: idempotencyKey === void 0 ? null : requestDigest,
521
+ retryTenantId: idempotencyKey === void 0 ? null : canonicalUuid(tenantId),
522
+ parentCommentId: parentCommentId ?? null,
523
+ status: participation.initialCommentStatus ?? "visible",
524
+ participationSnapshot: participation.snapshot ? JSON.stringify(participation.snapshot) : null
525
+ });
227
526
  });
527
+ return inTransaction ? guard.executeInTransaction(target.db, participationRequest, work) : guard.execute(participationRequest, work);
228
528
  };
229
529
  if (idempotencyKey === void 0) return insert(collection);
230
530
  const receipt = await runOnce({
@@ -237,7 +537,7 @@ var CommentService = class {
237
537
  const scoped = await CommentCollection.create({ db: tx });
238
538
  const matches = await tx.query(`SELECT CAST(id AS TEXT) AS id, CAST(tenant_id AS TEXT) AS tenant_id FROM "${scoped.tableName.replace(/"/g, "\"\"")}" WHERE retry_tenant_id = ? AND idempotency_key = ?`, canonicalUuid(tenantId), idempotencyKey);
239
539
  if (matches.rows.length > 1) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
240
- const row2 = matches.rows[0] ? await this.loadBoundComment(tx, matches.rows[0], canonicalUuid(tenantId)) : await insert(scoped);
540
+ const row2 = matches.rows[0] ? await this.loadBoundComment(tx, matches.rows[0], canonicalUuid(tenantId)) : await insert(scoped, true);
241
541
  return {
242
542
  id: row2.id,
243
543
  digest: row2.requestDigest,
@@ -246,17 +546,22 @@ var CommentService = class {
246
546
  });
247
547
  if (receipt.digest !== requestDigest) throw new CommentIdempotencyError("COMMENT_REQUEST_CONFLICT");
248
548
  const row = await this.loadBoundComment(collection.db, receipt, canonicalUuid(tenantId));
249
- if (!row || canonicalUuid(row.tenantId) !== canonicalUuid(tenantId) || row.idempotencyKey !== idempotencyKey || row.requestDigest !== requestDigest || digestRunOnceContent({
549
+ const retainedBodies = row.editedAt ? await withTenant({ tenantId: row.tenantId }, async () => (await (await CommentRevisionCollection.create({ db: collection.db })).list({ where: {
550
+ commentId: row.id,
551
+ tenantId: row.tenantId
552
+ } })).map((revision) => revision.body)) : [row.body];
553
+ if (canonicalUuid(row.tenantId) !== canonicalUuid(tenantId) || row.idempotencyKey !== idempotencyKey || row.requestDigest !== requestDigest || !retainedBodies.some((retainedBody) => digestRunOnceContent({
250
554
  tenantId: canonicalUuid(row.tenantId),
251
555
  authorUserId: canonicalUuid(row.authorUserId),
252
556
  metaType: row.metaType,
253
557
  metaId: row.metaId,
254
- body: row.body,
255
- mentions: [...row.mentionUserIds].sort()
256
- }) !== requestDigest) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
558
+ body: retainedBody,
559
+ mentions: [...row.mentionUserIds].sort(),
560
+ ...row.parentCommentId ? { parentCommentId: canonicalUuid(row.parentCommentId) } : {}
561
+ }) === requestDigest)) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
257
562
  return row;
258
563
  });
259
- if (!this.options.mentionNotifications || !comment.id) return comment;
564
+ if (!this.options.mentionNotifications || !comment.id || comment.status !== "visible") return comment;
260
565
  for (const recipientUserId of comment.mentionUserIds) {
261
566
  if (canonicalUuid(recipientUserId) === canonicalUuid(comment.authorUserId)) continue;
262
567
  await this.options.mentionNotifications.notifyMention({
@@ -288,6 +593,6 @@ function createCommentMentionNotifications(options) {
288
593
  } };
289
594
  }
290
595
  //#endregion
291
- export { Comment, CommentCollection, CommentIdempotencyError, CommentService, RecordCommentsRecipe, createCommentMentionNotifications };
596
+ export { Comment, CommentCollection, CommentIdempotencyError, CommentReport, CommentReportCollection, CommentRevision, CommentRevisionCollection, CommentService, ParticipationBan, ParticipationBanCollection, ParticipationDeniedError, ParticipationPolicy, ParticipationPolicyCollection, ParticipationRateBucket, ParticipationRateBucketCollection, ParticipationService, RecordCommentsRecipe, createCommentMentionNotifications };
292
597
 
293
598
  //# sourceMappingURL=index.js.map