@happyvertical/smrt-comments 0.0.0-stage → 0.55.11

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 (38) hide show
  1. package/AGENTS.md +17 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +58 -2
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/CommentCollection.d.ts +7 -0
  8. package/dist/collections/CommentCollection.d.ts.map +1 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +184 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/manifest.json +474 -0
  14. package/dist/models/Comment.d.ts +13 -0
  15. package/dist/models/Comment.d.ts.map +1 -0
  16. package/dist/recipes.d.ts +27 -0
  17. package/dist/recipes.d.ts.map +1 -0
  18. package/dist/services/CommentService.d.ts +16 -0
  19. package/dist/services/CommentService.d.ts.map +1 -0
  20. package/dist/services/mentionNotifications.d.ts +18 -0
  21. package/dist/services/mentionNotifications.d.ts.map +1 -0
  22. package/dist/smrt-knowledge.json +269 -0
  23. package/dist/svelte/RecordComments.svelte +71 -0
  24. package/dist/svelte/RecordComments.svelte.d.ts +16 -0
  25. package/dist/svelte/RecordComments.svelte.d.ts.map +1 -0
  26. package/dist/svelte/RecordCommentsPlayground.svelte +20 -0
  27. package/dist/svelte/RecordCommentsPlayground.svelte.d.ts +4 -0
  28. package/dist/svelte/RecordCommentsPlayground.svelte.d.ts.map +1 -0
  29. package/dist/svelte/demo.d.ts +4 -0
  30. package/dist/svelte/demo.d.ts.map +1 -0
  31. package/dist/svelte/demo.js +13 -0
  32. package/dist/svelte/index.d.ts +6 -0
  33. package/dist/svelte/index.d.ts.map +1 -0
  34. package/dist/svelte/index.js +4 -0
  35. package/dist/types.d.ts +31 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +0 -0
  38. package/package.json +62 -3
package/AGENTS.md ADDED
@@ -0,0 +1,17 @@
1
+ # @happyvertical/smrt-comments
2
+
3
+ Record discussions attach to arbitrary records through `Comment`'s inherited
4
+ `metaType` and `metaId`; preserve these as a qualified polymorphic pair and
5
+ never replace them with a text foreign key. `CommentService` owns persistence
6
+ and invokes the optional mention adapter only after the comment is saved.
7
+
8
+ The package must not depend on `smrt-messages`: hosts compose
9
+ `CommentMentionNotificationAdapter` with `UserNotificationService` and retain
10
+ their own notification delivery policy. The Svelte surface receives authorized
11
+ comment projections and callbacks; it does not fetch or authorize records.
12
+ `CommentService` requires a trusted actor and host record authorization callback.
13
+ `RecordComments.contextKey` includes tenant, actor and record identity.
14
+ See [test design](agents/test-design.md) for the behavior matrix.
15
+
16
+ Run `pnpm --filter @happyvertical/smrt-comments test`, `test:postgres`, `typecheck`, `check`, and
17
+ `npx --yes @biomejs/biome@2.5.2 check packages/comments` after changes.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # @happyvertical/smrt-comments
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Tenant-scoped polymorphic record comments, mention notification composition,
4
+ and a browser-safe `RecordComments` Svelte component.
5
+
6
+ Use `CommentService` on the server with a trusted `{ tenantId, userId }` actor
7
+ from the authenticated session and an `authorizeRecord` callback. The callback
8
+ must verify that the record belongs to that tenant and that the actor may read
9
+ or comment on it. Both `create` and `listForRecord` require that decision; request
10
+ bodies cannot override the actor. Direct model/collection APIs are persistence
11
+ primitives for trusted code, not authorization boundaries. Generated API, MCP,
12
+ and CLI access is disabled.
13
+
14
+ ```ts
15
+ const comments = new CommentService({
16
+ db,
17
+ actor: { tenantId: session.tenantId, userId: session.userId },
18
+ authorizeRecord: access => records.authorize(access),
19
+ mentionNotifications: createCommentMentionNotifications({
20
+ notifications: new UserNotificationService({ db }), // smrt-messages
21
+ canNotify: mention => records.recipientMayRead(mention),
22
+ }),
23
+ });
24
+ ```
25
+
26
+ The messages service is composed through a structural interface; there is no
27
+ runtime dependency on messages. `canNotify` must check recipient membership and
28
+ record access before exposing the comment. Notifications use a stable per-comment
29
+ source reference so adapter replays deduplicate. Mentions are UUIDs, deduplicated,
30
+ and limited to 50; self-mentions do not notify. No adapter means no notifications.
31
+ Mention validation finishes before the single comment insert; invalid or over-limit
32
+ mentions leave no saved row. The comment is saved before notification delivery. A delivery rejection leaves
33
+ the comment saved: reload it and retry delivery, not `create`, which creates a
34
+ new comment. The component warns users to refresh and check the discussion before
35
+ posting again when submission rejects. Delivery and persistence are deliberately not one transaction.
36
+
37
+ Import `RecordComments` from `@happyvertical/smrt-comments/svelte`. Pass authorized
38
+ comment projections, an `onsubmit(body)` callback, and a `contextKey` containing
39
+ the current tenant, actor and record identity. Change that key whenever any of
40
+ those identities changes: it clears drafts/errors and ignores stale submission
41
+ completion. The host owns loading, authentication, mention selection and refresh.
42
+ The component renders plain text. `RecordCommentsRecipe` declares the
43
+ `comments.records` model recipe in the stable `comments` catalog group. It
44
+ intentionally has no global navigation or section: a discussion needs an
45
+ already-authorized parent record, and a generic comment list would lose that
46
+ context. Hosts embed it on a record detail page. Catalog help explains posting
47
+ and mentions without promising a mention picker or delivery configuration.
48
+ The recipe declares `runtime: both`: the record component runs in the browser,
49
+ while persistence and authorization run on the server. Its playground surface
50
+ exports `RecordCommentsPlayground`, a standalone sample-record preview with
51
+ local-only posting. `recordCommentsDemo` supplies the same sample projections
52
+ through `demoSeed`; remounting resets the preview. No notifications are sent.
53
+ There is no route, settings panel or shell widget: none can supply the authorized
54
+ parent-record context. No provider is declared because the host's record access
55
+ and mention policies are callbacks, not a selectable external provider.
56
+
57
+ Validation: `pnpm --filter @happyvertical/smrt-comments test` covers SQLite and
58
+ DuckDB. `pnpm --filter @happyvertical/smrt-comments test:postgres` adds PostgreSQL
59
+ using the repository's disposable-database wrapper and ordinary CI role.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=__smrt-register__.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ import { SmrtCollection } from '@happyvertical/smrt-core';
2
+ import { Comment } from '../models/Comment.js';
3
+ export declare class CommentCollection extends SmrtCollection<Comment> {
4
+ static readonly _itemClass: typeof Comment;
5
+ listForRecord(recordType: string, recordId: string): Promise<Comment[]>;
6
+ }
7
+ //# sourceMappingURL=CommentCollection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentCollection.d.ts","sourceRoot":"","sources":["../../src/collections/CommentCollection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAK1D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,qBAAa,iBAAkB,SAAQ,cAAc,CAAC,OAAO,CAAC;IAC5D,MAAM,CAAC,QAAQ,CAAC,UAAU,iBAAW;IAE/B,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,EAAE,CAAC;CAYtB"}
@@ -0,0 +1,8 @@
1
+ import './__smrt-register__.js';
2
+ export { CommentCollection } from './collections/CommentCollection.js';
3
+ export { Comment } from './models/Comment.js';
4
+ export { RecordCommentsRecipe } from './recipes.js';
5
+ export { CommentService } from './services/CommentService.js';
6
+ export { type CommentNotificationService, createCommentMentionNotifications, } from './services/mentionNotifications.js';
7
+ export type { CommentActorContext, CommentMention, CommentMentionNotificationAdapter, CommentOptions, CommentRecordAccess, } from './types.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EACL,KAAK,0BAA0B,EAC/B,iCAAiC,GAClC,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,iCAAiC,EACjC,cAAc,EACd,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ import { ObjectRegistry, SmrtCollection, SmrtPolymorphicAssociation, SmrtRecipe, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
2
+ 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.55.11","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);","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}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"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":"78644ad4"},"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}},"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_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);","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}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"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":"78644ad4"},"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
+ //#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, {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value
14
+ }) : obj[key] = value;
15
+ var __decorateClass = (decorators, target, key, kind) => {
16
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
17
+ 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);
19
+ return result;
20
+ };
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+ var MAX_MENTIONS = 50;
23
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
+ var Comment = class extends SmrtPolymorphicAssociation {
25
+ tenantId = "";
26
+ authorUserId = "";
27
+ body = "";
28
+ /** JSON array of referenced User UUIDs; use mentionUserIds for validation. */
29
+ mentions = "[]";
30
+ constructor(options = {}) {
31
+ super(options);
32
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
33
+ if (options.authorUserId !== void 0) this.authorUserId = options.authorUserId;
34
+ if (options.body !== void 0) this.body = options.body;
35
+ const mentions = options.mentions;
36
+ if (Array.isArray(mentions)) this.mentionUserIds = mentions;
37
+ else if (typeof mentions === "string") this.mentions = mentions;
38
+ }
39
+ get mentionUserIds() {
40
+ try {
41
+ const parsed = JSON.parse(this.mentions);
42
+ return Array.isArray(parsed) && parsed.every((value) => typeof value === "string") ? parsed : [];
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
47
+ set mentionUserIds(value) {
48
+ const unique = [...new Set(value.map((id) => id.trim()).filter(Boolean))];
49
+ if (unique.length > MAX_MENTIONS || unique.some((id) => !UUID.test(id))) throw new TypeError("Comment mentions must contain at most 50 user UUIDs.");
50
+ this.mentions = JSON.stringify(unique);
51
+ }
52
+ };
53
+ __publicField(Comment, "__smrtPackage__", "@happyvertical/smrt-comments");
54
+ __decorateClass([tenantId()], Comment.prototype, "tenantId", 2);
55
+ __decorateClass([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Comment.prototype, "authorUserId", 2);
56
+ __decorateClass([field({ required: true })], Comment.prototype, "body", 2);
57
+ Comment = __decorateClass([TenantScoped({ mode: "required" }), smrt({
58
+ tableName: "record_comments",
59
+ api: false,
60
+ mcp: false,
61
+ cli: false,
62
+ indexes: [{
63
+ name: "record_comments_record_created_idx",
64
+ columns: [
65
+ "tenantId",
66
+ "metaType",
67
+ "metaId",
68
+ "createdAt"
69
+ ]
70
+ }]
71
+ })], Comment);
72
+ //#endregion
73
+ //#region src/collections/CommentCollection.ts
74
+ var CommentCollection = class extends SmrtCollection {
75
+ static _itemClass = Comment;
76
+ async listForRecord(recordType, recordId) {
77
+ const tenantId = getCurrentTenant()?.tenantId;
78
+ if (!tenantId) throw new TenantIsolationError("Record comments require a tenant context.");
79
+ return this.list({
80
+ where: {
81
+ tenantId,
82
+ metaType: recordType,
83
+ metaId: recordId
84
+ },
85
+ orderBy: ["createdAt ASC"]
86
+ });
87
+ }
88
+ };
89
+ //#endregion
90
+ //#region src/recipes.ts
91
+ var RecordCommentsRecipe = class extends SmrtRecipe {
92
+ static id = "comments.records";
93
+ static label = "Record comments";
94
+ static summary = "Discuss work directly on the records it concerns.";
95
+ static synonyms = [
96
+ "comments",
97
+ "mentions",
98
+ "discussion"
99
+ ];
100
+ static group = {
101
+ id: "comments",
102
+ label: "Comments",
103
+ summary: "Keep discussions alongside the records your team works on."
104
+ };
105
+ static models = [Comment];
106
+ static nav = [];
107
+ static runtime = "both";
108
+ static surfaces = [{
109
+ kind: "playground",
110
+ export: "@happyvertical/smrt-comments/svelte#RecordCommentsPlayground",
111
+ label: "Record discussion preview"
112
+ }];
113
+ static demoSeed = { export: "@happyvertical/smrt-comments/svelte#recordCommentsDemo" };
114
+ static help = "./comments.records.recipe.md";
115
+ };
116
+ //#endregion
117
+ //#region src/services/CommentService.ts
118
+ var CommentService = class {
119
+ constructor(options) {
120
+ this.options = options;
121
+ }
122
+ options;
123
+ async authorize(recordType, recordId, action) {
124
+ const { actor } = this.options;
125
+ if (!actor.tenantId || !actor.userId || !recordType || !recordId || !await this.options.authorizeRecord({
126
+ ...actor,
127
+ recordType,
128
+ recordId,
129
+ action
130
+ })) throw new Error("Comment record access denied.");
131
+ }
132
+ async listForRecord(recordType, recordId) {
133
+ await this.authorize(recordType, recordId, "read");
134
+ return withTenant({ tenantId: this.options.actor.tenantId }, async () => (await CommentCollection.create({ db: this.options.db })).listForRecord(recordType, recordId));
135
+ }
136
+ async create(commentOptions) {
137
+ if (!commentOptions.tenantId || !commentOptions.authorUserId) throw new TypeError("Comments require a tenant and author.");
138
+ if (commentOptions.tenantId !== this.options.actor.tenantId || commentOptions.authorUserId !== this.options.actor.userId) throw new Error("Comment actor context mismatch.");
139
+ await this.authorize(commentOptions.metaType ?? "", commentOptions.metaId ?? "", "comment");
140
+ if (!commentOptions.body?.trim()) throw new TypeError("Comment body is required.");
141
+ const { mentions, ...input } = commentOptions;
142
+ const prepared = new Comment();
143
+ if (mentions !== void 0) prepared.mentionUserIds = mentions;
144
+ const comment = await withTenant({ tenantId: commentOptions.tenantId }, async () => {
145
+ return (await CommentCollection.create({ db: this.options.db })).create({
146
+ ...input,
147
+ mentions: prepared.mentions
148
+ });
149
+ });
150
+ if (!this.options.mentionNotifications || !comment.id) return comment;
151
+ for (const recipientUserId of comment.mentionUserIds) {
152
+ if (recipientUserId === comment.authorUserId) continue;
153
+ await this.options.mentionNotifications.notifyMention({
154
+ commentId: comment.id,
155
+ tenantId: comment.tenantId,
156
+ authorUserId: comment.authorUserId,
157
+ recipientUserId,
158
+ recordType: comment.metaType,
159
+ recordId: comment.metaId,
160
+ body: comment.body
161
+ });
162
+ }
163
+ return comment;
164
+ }
165
+ };
166
+ //#endregion
167
+ //#region src/services/mentionNotifications.ts
168
+ function createCommentMentionNotifications(options) {
169
+ return { async notifyMention(mention) {
170
+ if (!await options.canNotify(mention)) return;
171
+ await options.notifications.notify({
172
+ tenantId: mention.tenantId,
173
+ recipientUserId: mention.recipientUserId,
174
+ kind: "comments.mention",
175
+ title: "You were mentioned in a comment",
176
+ body: mention.body,
177
+ sourceRef: `comments.mention:${mention.commentId}`
178
+ });
179
+ } };
180
+ }
181
+ //#endregion
182
+ export { Comment, CommentCollection, CommentService, RecordCommentsRecipe, createCommentMentionNotifications };
183
+
184
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/models/Comment.ts","../src/collections/CommentCollection.ts","../src/recipes.ts","../src/services/CommentService.ts","../src/services/mentionNotifications.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-comments\\\",\\\"packageVersion\\\":\\\"0.55.11\\\",\\\"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);\\\",\\\"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}},\\\"indexes\\\":[{\\\"name\\\":\\\"record_comments_slug_context_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"slug\\\",\\\"context\\\"],\\\"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\\\":\\\"78644ad4\\\"},\\\"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}},\\\"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_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);\\\",\\\"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}},\\\"indexes\\\":[{\\\"name\\\":\\\"record_comments_slug_context_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"slug\\\",\\\"context\\\"],\\\"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\\\":\\\"78644ad4\\\"},\\\"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}\"));\n","import {\n crossPackageRef,\n field,\n SmrtPolymorphicAssociation,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { CommentOptions } from '../types.js';\n\nconst MAX_MENTIONS = 50;\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n@TenantScoped({ mode: 'required' })\n@smrt({\n tableName: 'record_comments',\n api: false,\n mcp: false,\n cli: false,\n indexes: [\n {\n name: 'record_comments_record_created_idx',\n columns: ['tenantId', 'metaType', 'metaId', 'createdAt'],\n },\n ],\n})\nexport class Comment extends SmrtPolymorphicAssociation { static __smrtPackage__ = \"@happyvertical/smrt-comments\";\n @tenantId()\n tenantId = '';\n\n @crossPackageRef('@happyvertical/smrt-users:User', { required: true })\n authorUserId = '';\n\n @field({ required: true })\n body = '';\n\n /** JSON array of referenced User UUIDs; use mentionUserIds for validation. */\n mentions = '[]';\n\n constructor(options: CommentOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.authorUserId !== undefined)\n this.authorUserId = options.authorUserId;\n if (options.body !== undefined) this.body = options.body;\n const mentions = options.mentions as unknown;\n if (Array.isArray(mentions)) this.mentionUserIds = mentions;\n else if (typeof mentions === 'string') this.mentions = mentions;\n }\n\n get mentionUserIds(): string[] {\n try {\n const parsed: unknown = JSON.parse(this.mentions);\n return Array.isArray(parsed) &&\n parsed.every((value) => typeof value === 'string')\n ? parsed\n : [];\n } catch {\n return [];\n }\n }\n\n set mentionUserIds(value: readonly string[]) {\n const unique = [...new Set(value.map((id) => id.trim()).filter(Boolean))];\n if (unique.length > MAX_MENTIONS || unique.some((id) => !UUID.test(id))) {\n throw new TypeError(\n 'Comment mentions must contain at most 50 user UUIDs.',\n );\n }\n this.mentions = JSON.stringify(unique);\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n TenantIsolationError,\n} from '@happyvertical/smrt-tenancy';\nimport { Comment } from '../models/Comment.js';\n\nexport class CommentCollection extends SmrtCollection<Comment> {\n static readonly _itemClass = Comment;\n\n async listForRecord(\n recordType: string,\n recordId: string,\n ): Promise<Comment[]> {\n const tenantId = getCurrentTenant()?.tenantId;\n if (!tenantId) {\n throw new TenantIsolationError(\n 'Record comments require a tenant context.',\n );\n }\n return this.list({\n where: { tenantId, metaType: recordType, metaId: recordId },\n orderBy: ['createdAt ASC'],\n });\n }\n}\n","import { SmrtRecipe } from '@happyvertical/smrt-core';\nimport { Comment } from './models/Comment.js';\n\n/** Discussion attached to any authorized business record. */\nexport class RecordCommentsRecipe extends SmrtRecipe {\n static id = 'comments.records';\n static label = 'Record comments';\n static summary = 'Discuss work directly on the records it concerns.';\n static synonyms = ['comments', 'mentions', 'discussion'];\n static group = {\n id: 'comments',\n label: 'Comments',\n summary: 'Keep discussions alongside the records your team works on.',\n };\n static models = [Comment];\n // Record discussions need an authorized parent record, not a global list.\n static nav = [];\n static runtime = 'both' as const;\n static surfaces = [\n {\n kind: 'playground',\n export: '@happyvertical/smrt-comments/svelte#RecordCommentsPlayground',\n label: 'Record discussion preview',\n },\n ] as const;\n static demoSeed = {\n export: '@happyvertical/smrt-comments/svelte#recordCommentsDemo',\n } as const;\n static help = './comments.records.recipe.md';\n}\n","import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { withTenant } from '@happyvertical/smrt-tenancy';\nimport { CommentCollection } from '../collections/CommentCollection.js';\nimport { Comment } from '../models/Comment.js';\nimport type {\n CommentActorContext,\n CommentMentionNotificationAdapter,\n CommentOptions,\n CommentRecordAccess,\n} from '../types.js';\n\nexport class CommentService {\n constructor(\n private readonly options: {\n db: SmrtClassOptions['db'];\n actor: CommentActorContext;\n authorizeRecord: (access: CommentRecordAccess) => Promise<boolean>;\n mentionNotifications?: CommentMentionNotificationAdapter;\n },\n ) {}\n\n private async authorize(\n recordType: string,\n recordId: string,\n action: 'read' | 'comment',\n ) {\n const { actor } = this.options;\n if (\n !actor.tenantId ||\n !actor.userId ||\n !recordType ||\n !recordId ||\n !(await this.options.authorizeRecord({\n ...actor,\n recordType,\n recordId,\n action,\n }))\n ) {\n throw new Error('Comment record access denied.');\n }\n }\n\n async listForRecord(\n recordType: string,\n recordId: string,\n ): Promise<Comment[]> {\n await this.authorize(recordType, recordId, 'read');\n return withTenant({ tenantId: this.options.actor.tenantId }, async () =>\n (await CommentCollection.create({ db: this.options.db })).listForRecord(\n recordType,\n recordId,\n ),\n );\n }\n\n async create(commentOptions: CommentOptions): Promise<Comment> {\n if (!commentOptions.tenantId || !commentOptions.authorUserId) {\n throw new TypeError('Comments require a tenant and author.');\n }\n if (\n commentOptions.tenantId !== this.options.actor.tenantId ||\n commentOptions.authorUserId !== this.options.actor.userId\n ) {\n throw new Error('Comment actor context mismatch.');\n }\n await this.authorize(\n commentOptions.metaType ?? '',\n commentOptions.metaId ?? '',\n 'comment',\n );\n if (!commentOptions.body?.trim())\n throw new TypeError('Comment body is required.');\n const { mentions, ...input } = commentOptions;\n // Validate through the model's public setter before collection.create()\n // persists. Supply its normalized JSON in that single insert.\n const prepared = new Comment();\n if (mentions !== undefined) prepared.mentionUserIds = mentions;\n const comment = await withTenant(\n { tenantId: commentOptions.tenantId },\n async () => {\n return (await CommentCollection.create({ db: this.options.db })).create(\n {\n ...input,\n mentions: prepared.mentions,\n },\n );\n },\n );\n if (!this.options.mentionNotifications || !comment.id) return comment;\n for (const recipientUserId of comment.mentionUserIds) {\n if (recipientUserId === comment.authorUserId) continue;\n await this.options.mentionNotifications.notifyMention({\n commentId: comment.id,\n tenantId: comment.tenantId,\n authorUserId: comment.authorUserId,\n recipientUserId,\n recordType: comment.metaType,\n recordId: comment.metaId,\n body: comment.body,\n });\n }\n return comment;\n }\n}\n","import type {\n CommentMention,\n CommentMentionNotificationAdapter,\n} from '../types.js';\n\n/** Structural subset implemented by smrt-messages UserNotificationService. */\nexport interface CommentNotificationService {\n notify(input: {\n tenantId: string;\n recipientUserId: string;\n kind: string;\n title: string;\n body: string;\n sourceRef: string;\n }): Promise<unknown>;\n}\n\n/** Hosts must check recipient membership AND access to the referenced record. */\nexport function createCommentMentionNotifications(options: {\n notifications: CommentNotificationService;\n canNotify: (mention: CommentMention) => Promise<boolean>;\n}): CommentMentionNotificationAdapter {\n return {\n async notifyMention(mention) {\n if (!(await options.canNotify(mention))) return;\n await options.notifications.notify({\n tenantId: mention.tenantId,\n recipientUserId: mention.recipientUserId,\n kind: 'comments.mention',\n title: 'You were mentioned in a comment',\n body: mention.body,\n sourceRef: `comments.mention:${mention.commentId}`,\n });\n },\n };\n}\n"],"mappings":"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"}