@happyvertical/smrt-comments 0.55.18 → 0.55.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -8
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/services/CommentService.d.ts +2 -2
- package/dist/services/CommentService.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -7,7 +7,12 @@ Use `CommentService` on the server with a trusted `{ tenantId, userId }` actor
|
|
|
7
7
|
from the authenticated session and an `authorizeRecord` callback. The callback
|
|
8
8
|
must verify that the record belongs to that tenant and that the actor may read
|
|
9
9
|
or comment on it. Both `create` and `listForRecord` require that decision; request
|
|
10
|
-
bodies cannot override the actor.
|
|
10
|
+
bodies cannot override the actor. `CreateCommentInput` contains only tenant,
|
|
11
|
+
author, record type/id, body and mentions. The service ignores extra runtime
|
|
12
|
+
properties, generates its own UUID/slug and inserts a new row without natural-key
|
|
13
|
+
adoption. Caller-provided identity, context, database and constructor options
|
|
14
|
+
never reach persistence. `CommentOptions` remains the separate trusted model
|
|
15
|
+
hydration type. Direct model/collection APIs are persistence
|
|
11
16
|
primitives for trusted code, not authorization boundaries. Generated API, MCP,
|
|
12
17
|
and CLI access is disabled.
|
|
13
18
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export { Comment } from './models/Comment.js';
|
|
|
4
4
|
export { RecordCommentsRecipe } from './recipes.js';
|
|
5
5
|
export { CommentService } from './services/CommentService.js';
|
|
6
6
|
export { type CommentNotificationService, createCommentMentionNotifications, } from './services/mentionNotifications.js';
|
|
7
|
-
export type { CommentActorContext, CommentMention, CommentMentionNotificationAdapter, CommentOptions, CommentRecordAccess, } from './types.js';
|
|
7
|
+
export type { CommentActorContext, CommentMention, CommentMentionNotificationAdapter, CommentOptions, CommentRecordAccess, CreateCommentInput, } from './types.js';
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,EACnB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ObjectRegistry, SmrtCollection, SmrtPolymorphicAssociation, SmrtRecipe, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
|
|
2
2
|
import { TenantIsolationError, TenantScoped, getCurrentTenant, tenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
3
3
|
//#region src/__smrt-register__.ts
|
|
4
|
-
ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-comments","packageVersion":"0.55.
|
|
4
|
+
ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-comments","packageVersion":"0.55.20","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
5
|
//#endregion
|
|
6
6
|
//#region src/models/Comment.ts
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
@@ -134,16 +134,25 @@ var CommentService = class {
|
|
|
134
134
|
return withTenant({ tenantId: this.options.actor.tenantId }, async () => (await CommentCollection.create({ db: this.options.db })).listForRecord(recordType, recordId));
|
|
135
135
|
}
|
|
136
136
|
async create(commentOptions) {
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
const { tenantId, authorUserId, metaType, metaId, body, mentions } = commentOptions;
|
|
138
|
+
if (!tenantId || !authorUserId) throw new TypeError("Comments require a tenant and author.");
|
|
139
|
+
if (tenantId !== this.options.actor.tenantId || authorUserId !== this.options.actor.userId) throw new Error("Comment actor context mismatch.");
|
|
140
|
+
await this.authorize(metaType ?? "", metaId ?? "", "comment");
|
|
141
|
+
if (!body?.trim()) throw new TypeError("Comment body is required.");
|
|
142
142
|
const prepared = new Comment();
|
|
143
143
|
if (mentions !== void 0) prepared.mentionUserIds = mentions;
|
|
144
|
-
const
|
|
144
|
+
const id = crypto.randomUUID();
|
|
145
|
+
const comment = await withTenant({ tenantId }, async () => {
|
|
145
146
|
return (await CommentCollection.create({ db: this.options.db })).create({
|
|
146
|
-
|
|
147
|
+
tenantId,
|
|
148
|
+
authorUserId,
|
|
149
|
+
metaType,
|
|
150
|
+
metaId,
|
|
151
|
+
body,
|
|
152
|
+
id,
|
|
153
|
+
slug: id,
|
|
154
|
+
context: "",
|
|
155
|
+
_insertOnly: true,
|
|
147
156
|
mentions: prepared.mentions
|
|
148
157
|
});
|
|
149
158
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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.18\\\",\\\"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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"}
|
|
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.20\\\",\\\"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 CommentRecordAccess,\n CreateCommentInput,\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: CreateCommentInput): Promise<Comment> {\n const { tenantId, authorUserId, metaType, metaId, body, mentions } =\n commentOptions;\n if (!tenantId || !authorUserId) {\n throw new TypeError('Comments require a tenant and author.');\n }\n if (\n tenantId !== this.options.actor.tenantId ||\n authorUserId !== this.options.actor.userId\n ) {\n throw new Error('Comment actor context mismatch.');\n }\n await this.authorize(metaType ?? '', metaId ?? '', 'comment');\n if (!body?.trim()) throw new TypeError('Comment body is required.');\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 id = crypto.randomUUID();\n const comment = await withTenant({ tenantId }, async () => {\n return (await CommentCollection.create({ db: this.options.db })).create({\n tenantId,\n authorUserId,\n metaType,\n metaId,\n body,\n id,\n slug: id,\n context: '',\n _insertOnly: true,\n mentions: prepared.mentions,\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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"}
|
package/dist/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
2
|
import { Comment } from '../models/Comment.js';
|
|
3
|
-
import type { CommentActorContext, CommentMentionNotificationAdapter,
|
|
3
|
+
import type { CommentActorContext, CommentMentionNotificationAdapter, CommentRecordAccess, CreateCommentInput } from '../types.js';
|
|
4
4
|
export declare class CommentService {
|
|
5
5
|
private readonly options;
|
|
6
6
|
constructor(options: {
|
|
@@ -11,6 +11,6 @@ export declare class CommentService {
|
|
|
11
11
|
});
|
|
12
12
|
private authorize;
|
|
13
13
|
listForRecord(recordType: string, recordId: string): Promise<Comment[]>;
|
|
14
|
-
create(commentOptions:
|
|
14
|
+
create(commentOptions: CreateCommentInput): Promise<Comment>;
|
|
15
15
|
}
|
|
16
16
|
//# sourceMappingURL=CommentService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentService.d.ts","sourceRoot":"","sources":["../../src/services/CommentService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,KAAK,EACV,mBAAmB,EACnB,iCAAiC,EACjC,
|
|
1
|
+
{"version":3,"file":"CommentService.d.ts","sourceRoot":"","sources":["../../src/services/CommentService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,KAAK,EACV,mBAAmB,EACnB,iCAAiC,EACjC,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAErB,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE;QACxB,EAAE,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC3B,KAAK,EAAE,mBAAmB,CAAC;QAC3B,eAAe,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;QACnE,oBAAoB,CAAC,EAAE,iCAAiC,CAAC;KAC1D;YAGW,SAAS;IAsBjB,aAAa,CACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,EAAE,CAAC;IAUf,MAAM,CAAC,cAAc,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;CAgDnE"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-comments",
|
|
6
|
-
"packageVersion": "0.55.
|
|
6
|
+
"packageVersion": "0.55.20",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "b017910c9f20f1c00abdb9ce7d9ecc62f3babcc591eb36cc5356e0bd32a78543",
|
|
11
|
+
"packageJson": "ca99a4375f290436fce4f3601f0b99eaec45f9ef0190a8eb5256f4578b768413",
|
|
12
12
|
"agents": "06a1eb0f4777eea60f0ea72d08f5af82e5045f686b5197ddeaa2079e446852cd",
|
|
13
|
-
"moduleDoc:agents/test-design.md": "
|
|
13
|
+
"moduleDoc:agents/test-design.md": "0e6eb932f89676ae0e869b445be2e9a7dd100159dc850b6d992777645aa31851"
|
|
14
14
|
},
|
|
15
15
|
"exports": [
|
|
16
16
|
".",
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
{
|
|
211
211
|
"path": "agents/test-design.md",
|
|
212
212
|
"module": "test-design",
|
|
213
|
-
"content": "# Comments behavior evidence (#3714)\n\n| Behavior / trigger | Positive / failure | Actor / executor | Runtime / edge | Level / command |\n| --- | --- | --- | --- | --- |\n| Create/read a comment | Persist and reload; deny missing record permission, forged author or tenant | Trusted session actor; same supplied DB handle under tenant context | Node, SQLite/DuckDB/PostgreSQL; polymorphic record authorization callback | Integration, `test` / `test:postgres` |\n| Shared record identity in different tenants | Tenant A sees A; never B | Two tenant contexts, same actor and DB | All three SQL dialects; explicit tenant filter | Integration, `test` / `test:postgres` |\n| Mention creation | Deduplicate and exclude author; malformed UUID rejected | Authorized author, recipient access callback | All three SQL dialects; missing adapter is supported | Integration, `test` / `test:postgres` |\n| Mention delivery | Real UserNotificationService row; denied recipient gets none; replay deduplicates | Supplied notification service/DB | All three SQL dialects; upstream failure leaves saved comment | Integration, `test` / `test:postgres` |\n| UI submission | Trim/post; empty body and rejected callback keep actionable feedback | Host authorized projections | Browser Svelte; async callback | Component, `test` |\n| Actor/record switch during submission | Reset draft; stale success/error cannot mutate new draft | Changed composite contextKey | Browser Svelte; async completion | Component, `test` |\n| Recipe/schema | Recipe includes Comment; generated schema used by real persistence | Registry/test migrator | All three SQL dialects | Integration, `test` / `test:postgres`; `build` |\n\nNew feature, so a base-regression comparison is N/A. Comment insertion is one\nsave on the provided executor, with validation/authorization before mutation.\nNotification delivery intentionally occurs after save and is not atomic with it;\nthe failure test asserts the saved-row contract. No comment retry/idempotency\ncontract is promised: clients must not blindly retry create after delivery error.\nThe fixture-only playground does not replace the authenticated record surface.\n\nRecipe metadata follow-up (#3604): the comments catalog group, embedded-only\nnavigation and help are checked in the generated manifest and knowledge artifact\nafter building with refreshed scanner/core artifacts. Model exposure stays off.\nThis change does not affect persistence or delivery; prior three-dialect evidence\nremains applicable. Full package tests, typecheck, Svelte and Biome are rerun.\n\n#3708 follow-up: the browser-only playground renders fixture projections and\naccepts local posts without storage or notification access. Component tests\nverify display, posting, instance isolation and reset on remount. Build assertions\nverify runtime, playground and demo exports in manifest and knowledge artifacts.\nPersistence and authorization are unchanged; existing three-dialect evidence\napplies. Route/settings/shell/provider contracts are N/A because they cannot\nprovide the parent record authorization context.\n\n## Accepted review6084422109 round 1 regressions\n\n- Invalid UUID and 51 distinct mentions: authorized actor, each SQL dialect,\n failed create must leave zero rows for the record. Baseline failed on SQLite,\n DuckDB and PostgreSQL; public model setter now validates before the one insert.\n- Actual SQLite CommentService notification rejection composed with the Svelte\n submit callback: saved row remains, draft remains, alert instructs refresh and\n checking the discussion before reposting. Baseline produced “Try again”.\n- Commands: full comments `test`, `test:postgres`, `typecheck`, `check`, `build`,\n and Biome; evidence under `/private/tmp/smrt-track-b-evidence/3714-r1-*`.\n"
|
|
213
|
+
"content": "# Comments behavior evidence (#3714)\n\n| Behavior / trigger | Positive / failure | Actor / executor | Runtime / edge | Level / command |\n| --- | --- | --- | --- | --- |\n| Create/read a comment | Persist and reload; deny missing record permission, forged author or tenant | Trusted session actor; same supplied DB handle under tenant context | Node, SQLite/DuckDB/PostgreSQL; polymorphic record authorization callback | Integration, `test` / `test:postgres` |\n| Shared record identity in different tenants | Tenant A sees A; never B | Two tenant contexts, same actor and DB | All three SQL dialects; explicit tenant filter | Integration, `test` / `test:postgres` |\n| Mention creation | Deduplicate and exclude author; malformed UUID rejected | Authorized author, recipient access callback | All three SQL dialects; missing adapter is supported | Integration, `test` / `test:postgres` |\n| Mention delivery | Real UserNotificationService row; denied recipient gets none; replay deduplicates | Supplied notification service/DB | All three SQL dialects; upstream failure leaves saved comment | Integration, `test` / `test:postgres` |\n| UI submission | Trim/post; empty body and rejected callback keep actionable feedback | Host authorized projections | Browser Svelte; async callback | Component, `test` |\n| Actor/record switch during submission | Reset draft; stale success/error cannot mutate new draft | Changed composite contextKey | Browser Svelte; async completion | Component, `test` |\n| Recipe/schema | Recipe includes Comment; generated schema used by real persistence | Registry/test migrator | All three SQL dialects | Integration, `test` / `test:postgres`; `build` |\n\nNew feature, so a base-regression comparison is N/A. Comment insertion is one\nsave on the provided executor, with validation/authorization before mutation.\nNotification delivery intentionally occurs after save and is not atomic with it;\nthe failure test asserts the saved-row contract. No comment retry/idempotency\ncontract is promised: clients must not blindly retry create after delivery error.\nThe fixture-only playground does not replace the authenticated record surface.\n\nRecipe metadata follow-up (#3604): the comments catalog group, embedded-only\nnavigation and help are checked in the generated manifest and knowledge artifact\nafter building with refreshed scanner/core artifacts. Model exposure stays off.\nThis change does not affect persistence or delivery; prior three-dialect evidence\nremains applicable. Full package tests, typecheck, Svelte and Biome are rerun.\n\n#3708 follow-up: the browser-only playground renders fixture projections and\naccepts local posts without storage or notification access. Component tests\nverify display, posting, instance isolation and reset on remount. Build assertions\nverify runtime, playground and demo exports in manifest and knowledge artifacts.\nPersistence and authorization are unchanged; existing three-dialect evidence\napplies. Route/settings/shell/provider contracts are N/A because they cannot\nprovide the parent record authorization context.\n\n## Accepted review6084422109 round 1 regressions\n\n- Invalid UUID and 51 distinct mentions: authorized actor, each SQL dialect,\n failed create must leave zero rows for the record. Baseline failed on SQLite,\n DuckDB and PostgreSQL; public model setter now validates before the one insert.\n- Actual SQLite CommentService notification rejection composed with the Svelte\n submit callback: saved row remains, draft remains, alert instructs refresh and\n checking the discussion before reposting. Baseline produced “Try again”.\n- Commands: full comments `test`, `test:postgres`, `typecheck`, `check`, `build`,\n and Biome; evidence under `/private/tmp/smrt-track-b-evidence/3714-r1-*`.\n\n## Accepted review6084422109 round 6 persistence-key injection\n\nActor may comment on record A only; another author owns a comment on record B\nin the same tenant. Injecting B's slug/context, with and without its primary id,\nmust create a separate A row and preserve B's identity, author and body. Injected\ndatabase and constructor options must not reach persistence or alter defaults.\nReal SQLite/DuckDB/PostgreSQL baselines fail these assertions; the service now\nallowlists CreateCommentInput and generates identity with insert-only persistence.\nHydration options remain available only on the trusted model path. Full package,\nthree-dialect, type/Svelte, build and Biome evidence: `3714-round6-*` logs.\n"
|
|
214
214
|
}
|
|
215
215
|
],
|
|
216
216
|
"recipes": [
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export interface CommentOptions extends SmrtPolymorphicAssociationOptions {
|
|
|
5
5
|
body?: string;
|
|
6
6
|
mentions?: readonly string[];
|
|
7
7
|
}
|
|
8
|
+
/** Public service input; persistence identity and executor are server-owned. */
|
|
9
|
+
export interface CreateCommentInput {
|
|
10
|
+
tenantId: string;
|
|
11
|
+
authorUserId: string;
|
|
12
|
+
metaType: string;
|
|
13
|
+
metaId: string;
|
|
14
|
+
body: string;
|
|
15
|
+
mentions?: readonly string[];
|
|
16
|
+
}
|
|
8
17
|
export interface CommentMention {
|
|
9
18
|
commentId: string;
|
|
10
19
|
tenantId: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,0BAA0B,CAAC;AAElF,MAAM,WAAW,cAAe,SAAQ,iCAAiC;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,MAAM,WAAW,iCAAiC;IAChD,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,0BAA0B,CAAC;AAElF,MAAM,WAAW,cAAe,SAAQ,iCAAiC;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,gFAAgF;AAChF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,MAAM,WAAW,iCAAiC;IAChD,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-comments",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.20",
|
|
4
4
|
"description": "Record comments and mention hooks for SMRT applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"./manifest.json": "./dist/manifest.json"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@happyvertical/smrt-core": "0.55.
|
|
28
|
-
"@happyvertical/smrt-tenancy": "0.55.
|
|
27
|
+
"@happyvertical/smrt-core": "0.55.20",
|
|
28
|
+
"@happyvertical/smrt-tenancy": "0.55.20"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"svelte": "^5.57.0"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"directory": "packages/comments"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@happyvertical/smrt-vitest": "0.55.
|
|
46
|
+
"@happyvertical/smrt-vitest": "0.55.20",
|
|
47
47
|
"@sveltejs/package": "^2.5.8",
|
|
48
48
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
49
49
|
"@testing-library/svelte": "^5.4.2",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"typescript": "6.0.3",
|
|
54
54
|
"vite": "8.1.4",
|
|
55
55
|
"vitest": "4.1.10",
|
|
56
|
-
"@happyvertical/smrt-messages": "0.55.
|
|
56
|
+
"@happyvertical/smrt-messages": "0.55.20"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "vite build --mode library && svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json && node ../../scripts/prune-svelte-package-artifacts.js dist",
|