@happyvertical/smrt-comments 0.55.23 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/README.md +13 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -20
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +86 -5
- package/dist/models/Comment.d.ts +6 -0
- package/dist/models/Comment.d.ts.map +1 -1
- package/dist/services/CommentService.d.ts +7 -1
- package/dist/services/CommentService.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +34 -7
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/AGENTS.md
CHANGED
|
@@ -11,7 +11,7 @@ their own notification delivery policy. The Svelte surface receives authorized
|
|
|
11
11
|
comment projections and callbacks; it does not fetch or authorize records.
|
|
12
12
|
`CommentService` requires a trusted actor and host record authorization callback.
|
|
13
13
|
`RecordComments.contextKey` includes tenant, actor and record identity.
|
|
14
|
-
See [test design](agents/test-design.md) for
|
|
14
|
+
See [test design](agents/test-design.md) and [idempotent creation](agents/idempotent-create.md) for behavior matrices and retry invariants.
|
|
15
15
|
|
|
16
16
|
Run `pnpm --filter @happyvertical/smrt-comments test`, `test:postgres`, `typecheck`, `check`, and
|
|
17
17
|
`npx --yes @biomejs/biome@2.5.2 check packages/comments` after changes.
|
package/README.md
CHANGED
|
@@ -8,7 +8,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
10
|
bodies cannot override the actor. `CreateCommentInput` contains only tenant,
|
|
11
|
-
author, record type/id, body and
|
|
11
|
+
author, record type/id, body, mentions and an optional `idempotencyKey`. The service ignores extra runtime
|
|
12
12
|
properties, generates its own UUID/slug and inserts a new row without natural-key
|
|
13
13
|
adoption. Caller-provided identity, context, database and constructor options
|
|
14
14
|
never reach persistence. `CommentOptions` remains the separate trusted model
|
|
@@ -35,9 +35,12 @@ source reference so adapter replays deduplicate. Mentions are UUIDs, deduplicate
|
|
|
35
35
|
and limited to 50; self-mentions do not notify. No adapter means no notifications.
|
|
36
36
|
Mention validation finishes before the single comment insert; invalid or over-limit
|
|
37
37
|
mentions leave no saved row. The comment is saved before notification delivery. A delivery rejection leaves
|
|
38
|
-
the comment saved
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
the comment saved. With `idempotencyKey`, retry `create` with the same key and
|
|
39
|
+
content to recover delivery without creating another comment. Without a key,
|
|
40
|
+
reload and retry delivery rather than creating another comment. The component warns users to refresh and check the discussion before
|
|
41
|
+
posting again when submission rejects. Delivery and persistence are separate unless the host composes both with the same
|
|
42
|
+
outer transaction. See [idempotent creation](agents/idempotent-create.md) for key
|
|
43
|
+
binding, typed conflicts, retention and transaction requirements.
|
|
41
44
|
|
|
42
45
|
Import `RecordComments` from `@happyvertical/smrt-comments/svelte`. Pass authorized
|
|
43
46
|
comment projections, an `onsubmit(body)` callback, and a `contextKey` containing
|
|
@@ -62,3 +65,9 @@ and mention policies are callbacks, not a selectable external provider.
|
|
|
62
65
|
Validation: `pnpm --filter @happyvertical/smrt-comments test` covers SQLite and
|
|
63
66
|
DuckDB. `pnpm --filter @happyvertical/smrt-comments test:postgres` adds PostgreSQL
|
|
64
67
|
using the repository's disposable-database wrapper and ordinary CI role.
|
|
68
|
+
|
|
69
|
+
Keyed retries use a separate canonical tenant binding while preserving stored
|
|
70
|
+
tenant and author references. UUID casing changes across trusted actor contexts
|
|
71
|
+
reuse the same comment, including after generic receipt retention. Unkeyed legacy SQLite
|
|
72
|
+
listing remains scoped to its original tenant spelling; keyed rows are merged
|
|
73
|
+
by equivalent UUID tenant without folding arbitrary textual tenant IDs.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import './__smrt-register__.js';
|
|
|
2
2
|
export { CommentCollection } from './collections/CommentCollection.js';
|
|
3
3
|
export { Comment } from './models/Comment.js';
|
|
4
4
|
export { RecordCommentsRecipe } from './recipes.js';
|
|
5
|
-
export { CommentService } from './services/CommentService.js';
|
|
5
|
+
export { CommentIdempotencyError, CommentService, } from './services/CommentService.js';
|
|
6
6
|
export { type CommentNotificationService, createCommentMentionNotifications, } from './services/mentionNotifications.js';
|
|
7
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,
|
|
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,EACL,uBAAuB,EACvB,cAAc,GACf,MAAM,8BAA8B,CAAC;AACtC,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
|
-
import { ObjectRegistry, SmrtCollection, SmrtPolymorphicAssociation, SmrtRecipe, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
|
|
1
|
+
import { ObjectRegistry, SmrtCollection, SmrtPolymorphicAssociation, SmrtRecipe, crossPackageRef, digestRunOnceContent, field, runOnce, 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.23","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}`));
|
|
4
|
+
ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-comments","packageVersion":"0.56.0","objects":{"@happyvertical/smrt-comments:CommentCollection":{"name":"commentcollection","className":"CommentCollection","qualifiedName":"@happyvertical/smrt-comments:CommentCollection","collection":"comments","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/collections/CommentCollection.ts","packageName":"@happyvertical/smrt-comments","fields":{},"methods":{"listForRecord":{"name":"listForRecord","async":true,"parameters":[{"name":"recordType","type":"string","optional":false},{"name":"recordId","type":"string","optional":false}],"returnType":"Promise<Comment[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"record_comments","api":false,"mcp":false,"cli":false},"extends":"SmrtCollection","extendsTypeArg":"Comment","exportName":"CommentCollection","collectionExportName":"CommentCollectionCollection","schema":{"tableName":"record_comments","ddl":"CREATE TABLE IF NOT EXISTS \\"record_comments\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"meta_type\\" TEXT NOT NULL,\\n \\"meta_id\\" TEXT NOT NULL,\\n \\"role\\" TEXT NOT NULL,\\n \\"sort_order\\" INTEGER DEFAULT 0,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"author_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL,\\n \\"mentions\\" TEXT,\\n \\"idempotency_key\\" TEXT,\\n \\"retry_tenant_id\\" TEXT,\\n \\"request_digest\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"meta_type":{"type":"TEXT","notNull":true,"unique":false},"meta_id":{"type":"TEXT","notNull":true,"unique":false},"role":{"type":"TEXT","notNull":true,"unique":false},"sort_order":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"author_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false},"mentions":{"type":"TEXT","notNull":false,"unique":false},"idempotency_key":{"type":"TEXT","notNull":false,"unique":false},"retry_tenant_id":{"type":"TEXT","notNull":false,"unique":false},"request_digest":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"record_comments_tenant_request_key","columns":["retry_tenant_id","idempotency_key"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenant_id","meta_type","meta_id","created_at"]},{"name":"record_comments_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"record_comments_meta_type_meta_id_idx","columns":["meta_type","meta_id"]},{"name":"record_comments_author_user_id_idx","columns":["author_user_id"]}],"version":"5095dfc9"},"importPath":"@happyvertical/smrt-comments"},"@happyvertical/smrt-comments:Comment":{"name":"comment","className":"Comment","qualifiedName":"@happyvertical/smrt-comments:Comment","collection":"comments","filePath":"/home/runner/work/smrt/smrt/packages/comments/src/models/Comment.ts","packageName":"@happyvertical/smrt-comments","fields":{"metaType":{"type":"text","required":true,"_meta":{"required":true}},"metaId":{"type":"text","required":true,"_meta":{"required":true}},"role":{"type":"text","required":true,"_meta":{"required":true}},"sortOrder":{"type":"integer","required":false,"default":0},"tenantId":{"type":"text","required":true,"_meta":{"sqlType":"UUID","__tenancy":{"isTenantIdField":true,"autoFilter":true,"required":true,"autoPopulate":true,"nullable":false,"mode":"required","field":"tenantId","allowSuperAdminBypass":false}}},"authorUserId":{"type":"crossPackageRef","required":true,"related":"@happyvertical/smrt-users:User"},"body":{"type":"text","required":true,"_meta":{"required":true}},"mentions":{"type":"text","required":false},"idempotencyKey":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"retryTenantId":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true},"requestDigest":{"type":"text","required":false,"_meta":{"nullable":true,"readonly":true},"readonly":true}},"methods":{"hydrate":{"name":"hydrate","async":true,"parameters":[],"returnType":"Promise<T | null>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"record_comments","api":false,"mcp":false,"cli":false,"indexes":[{"name":"record_comments_tenant_request_key","columns":["retryTenantId","idempotencyKey"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenantId","metaType","metaId","createdAt"]}],"tenantScoped":{"mode":"required"},"conflictColumns":["tenant_id","slug","context"]},"extends":"SmrtPolymorphicAssociation","exportName":"Comment","collectionExportName":"CommentCollection","validationRules":[{"field":"metaType","rule":"required","fieldType":"text"},{"field":"metaId","rule":"required","fieldType":"text"},{"field":"role","rule":"required","fieldType":"text"},{"field":"authorUserId","rule":"required","fieldType":"crossPackageRef"},{"field":"body","rule":"required","fieldType":"text"}],"schema":{"tableName":"record_comments","ddl":"CREATE TABLE IF NOT EXISTS \\"record_comments\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"meta_type\\" TEXT NOT NULL,\\n \\"meta_id\\" TEXT NOT NULL,\\n \\"role\\" TEXT NOT NULL,\\n \\"sort_order\\" INTEGER DEFAULT 0,\\n \\"tenant_id\\" UUID NOT NULL,\\n \\"author_user_id\\" UUID NOT NULL,\\n \\"body\\" TEXT NOT NULL,\\n \\"mentions\\" TEXT,\\n \\"idempotency_key\\" TEXT,\\n \\"retry_tenant_id\\" TEXT,\\n \\"request_digest\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"meta_type":{"type":"TEXT","notNull":true,"unique":false},"meta_id":{"type":"TEXT","notNull":true,"unique":false},"role":{"type":"TEXT","notNull":true,"unique":false},"sort_order":{"type":"INTEGER","notNull":false,"unique":false,"default":0},"tenant_id":{"type":"UUID","referenceKind":"tenantId","notNull":true,"unique":false},"author_user_id":{"type":"UUID","referenceKind":"crossPackageRef","notNull":true,"unique":false},"body":{"type":"TEXT","notNull":true,"unique":false},"mentions":{"type":"TEXT","notNull":false,"unique":false},"idempotency_key":{"type":"TEXT","notNull":false,"unique":false},"retry_tenant_id":{"type":"TEXT","notNull":false,"unique":false},"request_digest":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"record_comments_slug_context_idx","columns":["tenant_id","slug","context"],"unique":true},{"name":"record_comments_tenant_request_key","columns":["retry_tenant_id","idempotency_key"],"unique":true},{"name":"record_comments_record_created_idx","columns":["tenant_id","meta_type","meta_id","created_at"]},{"name":"record_comments_tenant_id_created_at_idx","columns":["tenant_id","created_at"]},{"name":"record_comments_meta_type_meta_id_idx","columns":["meta_type","meta_id"]},{"name":"record_comments_author_user_id_idx","columns":["author_user_id"]}],"version":"5095dfc9"},"importPath":"@happyvertical/smrt-comments"}},"moduleType":"smrt","recipes":[{"id":"comments.records","className":"RecordCommentsRecipe","label":"Record comments","summary":"Discuss work directly on the records it concerns.","synonyms":["comments","mentions","discussion"],"models":["@happyvertical/smrt-comments:Comment"],"nav":[],"requires":[],"group":{"id":"comments","label":"Comments","summary":"Keep discussions alongside the records your team works on."},"help":{"markdown":"## Overview\\n\\nRecord comments keep a discussion next to the work it concerns. Open a record\\nyou can access to read its discussion or add a comment. Comments are part of\\nthe record page, rather than a separate list of every discussion.\\n\\n## Tasks\\n\\n### Add a comment\\n\\nOpen the record, find Comments, write your message and choose Post comment.\\nYour app controls who can read the record and who can contribute to its discussion.\\n\\n### Mention someone\\n\\nIf your app provides mention selection, choose a teammate who can access the\\nrecord. When mention notifications are connected, that person receives a\\nnotification. Typing a name alone does not select a recipient.\\n\\n### Check a failed post\\n\\nIf posting reports an error, refresh the discussion before trying again. A\\ncomment may have been saved even when its mention notification could not be\\ndelivered. If it appears in the discussion, do not post it a second time.\\n","fieldRefs":[]},"surfaces":[{"kind":"playground","export":"@happyvertical/smrt-comments/svelte#RecordCommentsPlayground","label":"Record discussion preview"}],"runtime":"both","demoSeed":{"export":"@happyvertical/smrt-comments/svelte#recordCommentsDemo"},"demo":{"mode":"server","reasons":["Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."]}}],"smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-messages","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3624"],"via":["@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
|
|
5
5
|
//#endregion
|
|
6
6
|
//#region src/models/Comment.ts
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
@@ -27,11 +27,17 @@ var Comment = class extends SmrtPolymorphicAssociation {
|
|
|
27
27
|
body = "";
|
|
28
28
|
/** JSON array of referenced User UUIDs; use mentionUserIds for validation. */
|
|
29
29
|
mentions = "[]";
|
|
30
|
+
idempotencyKey = null;
|
|
31
|
+
retryTenantId = null;
|
|
32
|
+
requestDigest = null;
|
|
30
33
|
constructor(options = {}) {
|
|
31
34
|
super(options);
|
|
32
35
|
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
33
36
|
if (options.authorUserId !== void 0) this.authorUserId = options.authorUserId;
|
|
34
37
|
if (options.body !== void 0) this.body = options.body;
|
|
38
|
+
if (options.idempotencyKey !== void 0) this.idempotencyKey = options.idempotencyKey;
|
|
39
|
+
if (options.retryTenantId !== void 0) this.retryTenantId = options.retryTenantId;
|
|
40
|
+
if (options.requestDigest !== void 0) this.requestDigest = options.requestDigest;
|
|
35
41
|
const mentions = options.mentions;
|
|
36
42
|
if (Array.isArray(mentions)) this.mentionUserIds = mentions;
|
|
37
43
|
else if (typeof mentions === "string") this.mentions = mentions;
|
|
@@ -54,12 +60,31 @@ __publicField(Comment, "__smrtPackage__", "@happyvertical/smrt-comments");
|
|
|
54
60
|
__decorateClass([tenantId()], Comment.prototype, "tenantId", 2);
|
|
55
61
|
__decorateClass([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Comment.prototype, "authorUserId", 2);
|
|
56
62
|
__decorateClass([field({ required: true })], Comment.prototype, "body", 2);
|
|
63
|
+
__decorateClass([field({
|
|
64
|
+
type: "text",
|
|
65
|
+
nullable: true,
|
|
66
|
+
readonly: true
|
|
67
|
+
})], Comment.prototype, "idempotencyKey", 2);
|
|
68
|
+
__decorateClass([field({
|
|
69
|
+
type: "text",
|
|
70
|
+
nullable: true,
|
|
71
|
+
readonly: true
|
|
72
|
+
})], Comment.prototype, "retryTenantId", 2);
|
|
73
|
+
__decorateClass([field({
|
|
74
|
+
type: "text",
|
|
75
|
+
nullable: true,
|
|
76
|
+
readonly: true
|
|
77
|
+
})], Comment.prototype, "requestDigest", 2);
|
|
57
78
|
Comment = __decorateClass([TenantScoped({ mode: "required" }), smrt({
|
|
58
79
|
tableName: "record_comments",
|
|
59
80
|
api: false,
|
|
60
81
|
mcp: false,
|
|
61
82
|
cli: false,
|
|
62
83
|
indexes: [{
|
|
84
|
+
name: "record_comments_tenant_request_key",
|
|
85
|
+
columns: ["retryTenantId", "idempotencyKey"],
|
|
86
|
+
unique: true
|
|
87
|
+
}, {
|
|
63
88
|
name: "record_comments_record_created_idx",
|
|
64
89
|
columns: [
|
|
65
90
|
"tenantId",
|
|
@@ -115,6 +140,15 @@ var RecordCommentsRecipe = class extends SmrtRecipe {
|
|
|
115
140
|
};
|
|
116
141
|
//#endregion
|
|
117
142
|
//#region src/services/CommentService.ts
|
|
143
|
+
var canonicalUuid = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value) ? value.toLowerCase() : value;
|
|
144
|
+
var CommentIdempotencyError = class extends Error {
|
|
145
|
+
constructor(code) {
|
|
146
|
+
super(code === "COMMENT_REQUEST_CONFLICT" ? "Comment request key already used with different content." : "Comment request no longer has an unchanged comment.");
|
|
147
|
+
this.code = code;
|
|
148
|
+
this.name = "CommentIdempotencyError";
|
|
149
|
+
}
|
|
150
|
+
code;
|
|
151
|
+
};
|
|
118
152
|
var CommentService = class {
|
|
119
153
|
constructor(options) {
|
|
120
154
|
this.options = options;
|
|
@@ -131,34 +165,100 @@ var CommentService = class {
|
|
|
131
165
|
}
|
|
132
166
|
async listForRecord(recordType, recordId) {
|
|
133
167
|
await this.authorize(recordType, recordId, "read");
|
|
134
|
-
|
|
168
|
+
const tenantId = this.options.actor.tenantId;
|
|
169
|
+
const binding = canonicalUuid(tenantId);
|
|
170
|
+
const rows = await withTenant({ tenantId }, async () => (await CommentCollection.create({ db: this.options.db })).listForRecord(recordType, recordId));
|
|
171
|
+
const collection = await withTenant({ tenantId }, () => CommentCollection.create({ db: this.options.db }));
|
|
172
|
+
const bound = await collection.db.query(`SELECT CAST(id AS TEXT) AS id, CAST(tenant_id AS TEXT) AS tenant_id FROM "${collection.tableName.replace(/"/g, "\"\"")}" WHERE retry_tenant_id = ? AND meta_type = ? AND meta_id = ? AND idempotency_key IS NOT NULL`, binding, recordType, recordId);
|
|
173
|
+
for (const stored of bound.rows) {
|
|
174
|
+
const row = await this.loadBoundComment(collection.db, stored, binding);
|
|
175
|
+
if (row.metaType !== recordType || row.metaId !== recordId) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
|
|
176
|
+
rows.push(row);
|
|
177
|
+
}
|
|
178
|
+
for (const row of rows) if (row.idempotencyKey !== null && row.retryTenantId !== binding) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
|
|
179
|
+
return [...new Map(rows.map((row) => [row.id, row])).values()].sort((a, b) => (a.created_at?.getTime() ?? 0) - (b.created_at?.getTime() ?? 0) || (a.id ?? "").localeCompare(b.id ?? ""));
|
|
180
|
+
}
|
|
181
|
+
async loadBoundComment(db, stored, binding) {
|
|
182
|
+
const id = stored.id;
|
|
183
|
+
const tenantId = stored.tenant_id;
|
|
184
|
+
if (typeof id !== "string" || typeof tenantId !== "string" || canonicalUuid(tenantId) !== binding) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
|
|
185
|
+
const row = await withTenant({ tenantId }, async () => (await CommentCollection.create({ db })).get(id));
|
|
186
|
+
if (!row || canonicalUuid(row.tenantId) !== binding || row.retryTenantId !== binding || row.idempotencyKey === null) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
|
|
187
|
+
return row;
|
|
135
188
|
}
|
|
136
189
|
async create(commentOptions) {
|
|
137
|
-
const { tenantId, authorUserId, metaType, metaId, body, mentions } = commentOptions;
|
|
138
|
-
if (!
|
|
139
|
-
if (
|
|
190
|
+
const { tenantId: inputTenantId, authorUserId: inputAuthorUserId, metaType, metaId, body, mentions, idempotencyKey } = commentOptions;
|
|
191
|
+
if (!inputTenantId || !inputAuthorUserId) throw new TypeError("Comments require a tenant and author.");
|
|
192
|
+
if (canonicalUuid(inputTenantId) !== canonicalUuid(this.options.actor.tenantId) || canonicalUuid(inputAuthorUserId) !== canonicalUuid(this.options.actor.userId)) throw new Error("Comment actor context mismatch.");
|
|
193
|
+
const tenantId = this.options.actor.tenantId;
|
|
194
|
+
const authorUserId = this.options.actor.userId;
|
|
140
195
|
await this.authorize(metaType ?? "", metaId ?? "", "comment");
|
|
141
196
|
if (!body?.trim()) throw new TypeError("Comment body is required.");
|
|
142
197
|
const prepared = new Comment();
|
|
143
198
|
if (mentions !== void 0) prepared.mentionUserIds = mentions;
|
|
144
|
-
|
|
199
|
+
if (idempotencyKey !== void 0 && (typeof idempotencyKey !== "string" || !idempotencyKey.trim() || idempotencyKey !== idempotencyKey.trim() || idempotencyKey.length > 200)) throw new TypeError("Comment idempotencyKey must be a nonempty trimmed string of at most 200 characters.");
|
|
200
|
+
const normalizedMentions = [...prepared.mentionUserIds].sort();
|
|
201
|
+
const requestDigest = digestRunOnceContent({
|
|
202
|
+
tenantId: canonicalUuid(tenantId),
|
|
203
|
+
authorUserId: canonicalUuid(authorUserId),
|
|
204
|
+
metaType,
|
|
205
|
+
metaId,
|
|
206
|
+
body,
|
|
207
|
+
mentions: normalizedMentions
|
|
208
|
+
});
|
|
145
209
|
const comment = await withTenant({ tenantId }, async () => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
210
|
+
const collection = await CommentCollection.create({ db: this.options.db });
|
|
211
|
+
const insert = async (target) => {
|
|
212
|
+
const id = crypto.randomUUID();
|
|
213
|
+
return target.create({
|
|
214
|
+
tenantId,
|
|
215
|
+
authorUserId,
|
|
216
|
+
metaType,
|
|
217
|
+
metaId,
|
|
218
|
+
body,
|
|
219
|
+
id,
|
|
220
|
+
slug: id,
|
|
221
|
+
context: "",
|
|
222
|
+
_insertOnly: true,
|
|
223
|
+
mentions: prepared.mentions,
|
|
224
|
+
idempotencyKey: idempotencyKey ?? null,
|
|
225
|
+
requestDigest: idempotencyKey === void 0 ? null : requestDigest,
|
|
226
|
+
retryTenantId: idempotencyKey === void 0 ? null : canonicalUuid(tenantId)
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
if (idempotencyKey === void 0) return insert(collection);
|
|
230
|
+
const receipt = await runOnce({
|
|
231
|
+
db: collection.db,
|
|
232
|
+
tenantId: canonicalUuid(tenantId),
|
|
233
|
+
actor: "smrt-comments.create:v1",
|
|
234
|
+
token: idempotencyKey,
|
|
235
|
+
content: "comment-request-binding:v1"
|
|
236
|
+
}, async (tx) => {
|
|
237
|
+
const scoped = await CommentCollection.create({ db: tx });
|
|
238
|
+
const matches = await tx.query(`SELECT CAST(id AS TEXT) AS id, CAST(tenant_id AS TEXT) AS tenant_id FROM "${scoped.tableName.replace(/"/g, "\"\"")}" WHERE retry_tenant_id = ? AND idempotency_key = ?`, canonicalUuid(tenantId), idempotencyKey);
|
|
239
|
+
if (matches.rows.length > 1) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
|
|
240
|
+
const row2 = matches.rows[0] ? await this.loadBoundComment(tx, matches.rows[0], canonicalUuid(tenantId)) : await insert(scoped);
|
|
241
|
+
return {
|
|
242
|
+
id: row2.id,
|
|
243
|
+
digest: row2.requestDigest,
|
|
244
|
+
tenant_id: row2.tenantId
|
|
245
|
+
};
|
|
157
246
|
});
|
|
247
|
+
if (receipt.digest !== requestDigest) throw new CommentIdempotencyError("COMMENT_REQUEST_CONFLICT");
|
|
248
|
+
const row = await this.loadBoundComment(collection.db, receipt, canonicalUuid(tenantId));
|
|
249
|
+
if (!row || canonicalUuid(row.tenantId) !== canonicalUuid(tenantId) || row.idempotencyKey !== idempotencyKey || row.requestDigest !== requestDigest || digestRunOnceContent({
|
|
250
|
+
tenantId: canonicalUuid(row.tenantId),
|
|
251
|
+
authorUserId: canonicalUuid(row.authorUserId),
|
|
252
|
+
metaType: row.metaType,
|
|
253
|
+
metaId: row.metaId,
|
|
254
|
+
body: row.body,
|
|
255
|
+
mentions: [...row.mentionUserIds].sort()
|
|
256
|
+
}) !== requestDigest) throw new CommentIdempotencyError("COMMENT_REQUEST_UNAVAILABLE");
|
|
257
|
+
return row;
|
|
158
258
|
});
|
|
159
259
|
if (!this.options.mentionNotifications || !comment.id) return comment;
|
|
160
260
|
for (const recipientUserId of comment.mentionUserIds) {
|
|
161
|
-
if (recipientUserId === comment.authorUserId) continue;
|
|
261
|
+
if (canonicalUuid(recipientUserId) === canonicalUuid(comment.authorUserId)) continue;
|
|
162
262
|
await this.options.mentionNotifications.notifyMention({
|
|
163
263
|
commentId: comment.id,
|
|
164
264
|
tenantId: comment.tenantId,
|
|
@@ -188,6 +288,6 @@ function createCommentMentionNotifications(options) {
|
|
|
188
288
|
} };
|
|
189
289
|
}
|
|
190
290
|
//#endregion
|
|
191
|
-
export { Comment, CommentCollection, CommentService, RecordCommentsRecipe, createCommentMentionNotifications };
|
|
291
|
+
export { Comment, CommentCollection, CommentIdempotencyError, CommentService, RecordCommentsRecipe, createCommentMentionNotifications };
|
|
192
292
|
|
|
193
293
|
//# sourceMappingURL=index.js.map
|
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.23\\\",\\\"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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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["row"],"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.56.0\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-comments:CommentCollection\\\":{\\\"name\\\":\\\"commentcollection\\\",\\\"className\\\":\\\"CommentCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-comments:CommentCollection\\\",\\\"collection\\\":\\\"comments\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/comments/src/collections/CommentCollection.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-comments\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"listForRecord\\\":{\\\"name\\\":\\\"listForRecord\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"recordType\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"recordId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<Comment[]>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"record_comments\\\",\\\"api\\\":false,\\\"mcp\\\":false,\\\"cli\\\":false},\\\"extends\\\":\\\"SmrtCollection\\\",\\\"extendsTypeArg\\\":\\\"Comment\\\",\\\"exportName\\\":\\\"CommentCollection\\\",\\\"collectionExportName\\\":\\\"CommentCollectionCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"record_comments\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"record_comments\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"meta_type\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"meta_id\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"role\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"sort_order\\\\\\\" INTEGER DEFAULT 0,\\\\n \\\\\\\"tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"author_user_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"body\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"mentions\\\\\\\" TEXT,\\\\n \\\\\\\"idempotency_key\\\\\\\" TEXT,\\\\n \\\\\\\"retry_tenant_id\\\\\\\" TEXT,\\\\n \\\\\\\"request_digest\\\\\\\" TEXT\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"meta_type\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"meta_id\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"role\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"sort_order\\\":{\\\"type\\\":\\\"INTEGER\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"default\\\":0},\\\"tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"tenantId\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"author_user_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"body\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"mentions\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"idempotency_key\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"retry_tenant_id\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"request_digest\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false}},\\\"indexes\\\":[{\\\"name\\\":\\\"record_comments_slug_context_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"slug\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"record_comments_tenant_request_key\\\",\\\"columns\\\":[\\\"retry_tenant_id\\\",\\\"idempotency_key\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"record_comments_record_created_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"meta_type\\\",\\\"meta_id\\\",\\\"created_at\\\"]},{\\\"name\\\":\\\"record_comments_tenant_id_created_at_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"created_at\\\"]},{\\\"name\\\":\\\"record_comments_meta_type_meta_id_idx\\\",\\\"columns\\\":[\\\"meta_type\\\",\\\"meta_id\\\"]},{\\\"name\\\":\\\"record_comments_author_user_id_idx\\\",\\\"columns\\\":[\\\"author_user_id\\\"]}],\\\"version\\\":\\\"5095dfc9\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-comments\\\"},\\\"@happyvertical/smrt-comments:Comment\\\":{\\\"name\\\":\\\"comment\\\",\\\"className\\\":\\\"Comment\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-comments:Comment\\\",\\\"collection\\\":\\\"comments\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/comments/src/models/Comment.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-comments\\\",\\\"fields\\\":{\\\"metaType\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"required\\\":true}},\\\"metaId\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"required\\\":true}},\\\"role\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"required\\\":true}},\\\"sortOrder\\\":{\\\"type\\\":\\\"integer\\\",\\\"required\\\":false,\\\"default\\\":0},\\\"tenantId\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"sqlType\\\":\\\"UUID\\\",\\\"__tenancy\\\":{\\\"isTenantIdField\\\":true,\\\"autoFilter\\\":true,\\\"required\\\":true,\\\"autoPopulate\\\":true,\\\"nullable\\\":false,\\\"mode\\\":\\\"required\\\",\\\"field\\\":\\\"tenantId\\\",\\\"allowSuperAdminBypass\\\":false}}},\\\"authorUserId\\\":{\\\"type\\\":\\\"crossPackageRef\\\",\\\"required\\\":true,\\\"related\\\":\\\"@happyvertical/smrt-users:User\\\"},\\\"body\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"required\\\":true}},\\\"mentions\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false},\\\"idempotencyKey\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true,\\\"readonly\\\":true},\\\"readonly\\\":true},\\\"retryTenantId\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true,\\\"readonly\\\":true},\\\"readonly\\\":true},\\\"requestDigest\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true,\\\"readonly\\\":true},\\\"readonly\\\":true}},\\\"methods\\\":{\\\"hydrate\\\":{\\\"name\\\":\\\"hydrate\\\",\\\"async\\\":true,\\\"parameters\\\":[],\\\"returnType\\\":\\\"Promise<T | null>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"record_comments\\\",\\\"api\\\":false,\\\"mcp\\\":false,\\\"cli\\\":false,\\\"indexes\\\":[{\\\"name\\\":\\\"record_comments_tenant_request_key\\\",\\\"columns\\\":[\\\"retryTenantId\\\",\\\"idempotencyKey\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"record_comments_record_created_idx\\\",\\\"columns\\\":[\\\"tenantId\\\",\\\"metaType\\\",\\\"metaId\\\",\\\"createdAt\\\"]}],\\\"tenantScoped\\\":{\\\"mode\\\":\\\"required\\\"},\\\"conflictColumns\\\":[\\\"tenant_id\\\",\\\"slug\\\",\\\"context\\\"]},\\\"extends\\\":\\\"SmrtPolymorphicAssociation\\\",\\\"exportName\\\":\\\"Comment\\\",\\\"collectionExportName\\\":\\\"CommentCollection\\\",\\\"validationRules\\\":[{\\\"field\\\":\\\"metaType\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"metaId\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"role\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"authorUserId\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"crossPackageRef\\\"},{\\\"field\\\":\\\"body\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"}],\\\"schema\\\":{\\\"tableName\\\":\\\"record_comments\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"record_comments\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"meta_type\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"meta_id\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"role\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"sort_order\\\\\\\" INTEGER DEFAULT 0,\\\\n \\\\\\\"tenant_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"author_user_id\\\\\\\" UUID NOT NULL,\\\\n \\\\\\\"body\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"mentions\\\\\\\" TEXT,\\\\n \\\\\\\"idempotency_key\\\\\\\" TEXT,\\\\n \\\\\\\"retry_tenant_id\\\\\\\" TEXT,\\\\n \\\\\\\"request_digest\\\\\\\" TEXT\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"meta_type\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"meta_id\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"role\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"sort_order\\\":{\\\"type\\\":\\\"INTEGER\\\",\\\"notNull\\\":false,\\\"unique\\\":false,\\\"default\\\":0},\\\"tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"tenantId\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"author_user_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"referenceKind\\\":\\\"crossPackageRef\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"body\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"mentions\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"idempotency_key\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"retry_tenant_id\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"request_digest\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false}},\\\"indexes\\\":[{\\\"name\\\":\\\"record_comments_slug_context_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"slug\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"record_comments_tenant_request_key\\\",\\\"columns\\\":[\\\"retry_tenant_id\\\",\\\"idempotency_key\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"record_comments_record_created_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"meta_type\\\",\\\"meta_id\\\",\\\"created_at\\\"]},{\\\"name\\\":\\\"record_comments_tenant_id_created_at_idx\\\",\\\"columns\\\":[\\\"tenant_id\\\",\\\"created_at\\\"]},{\\\"name\\\":\\\"record_comments_meta_type_meta_id_idx\\\",\\\"columns\\\":[\\\"meta_type\\\",\\\"meta_id\\\"]},{\\\"name\\\":\\\"record_comments_author_user_id_idx\\\",\\\"columns\\\":[\\\"author_user_id\\\"]}],\\\"version\\\":\\\"5095dfc9\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-comments\\\"}},\\\"moduleType\\\":\\\"smrt\\\",\\\"recipes\\\":[{\\\"id\\\":\\\"comments.records\\\",\\\"className\\\":\\\"RecordCommentsRecipe\\\",\\\"label\\\":\\\"Record comments\\\",\\\"summary\\\":\\\"Discuss work directly on the records it concerns.\\\",\\\"synonyms\\\":[\\\"comments\\\",\\\"mentions\\\",\\\"discussion\\\"],\\\"models\\\":[\\\"@happyvertical/smrt-comments:Comment\\\"],\\\"nav\\\":[],\\\"requires\\\":[],\\\"group\\\":{\\\"id\\\":\\\"comments\\\",\\\"label\\\":\\\"Comments\\\",\\\"summary\\\":\\\"Keep discussions alongside the records your team works on.\\\"},\\\"help\\\":{\\\"markdown\\\":\\\"## Overview\\\\n\\\\nRecord comments keep a discussion next to the work it concerns. Open a record\\\\nyou can access to read its discussion or add a comment. Comments are part of\\\\nthe record page, rather than a separate list of every discussion.\\\\n\\\\n## Tasks\\\\n\\\\n### Add a comment\\\\n\\\\nOpen the record, find Comments, write your message and choose Post comment.\\\\nYour app controls who can read the record and who can contribute to its discussion.\\\\n\\\\n### Mention someone\\\\n\\\\nIf your app provides mention selection, choose a teammate who can access the\\\\nrecord. When mention notifications are connected, that person receives a\\\\nnotification. Typing a name alone does not select a recipient.\\\\n\\\\n### Check a failed post\\\\n\\\\nIf posting reports an error, refresh the discussion before trying again. A\\\\ncomment may have been saved even when its mention notification could not be\\\\ndelivered. If it appears in the discussion, do not post it a second time.\\\\n\\\",\\\"fieldRefs\\\":[]},\\\"surfaces\\\":[{\\\"kind\\\":\\\"playground\\\",\\\"export\\\":\\\"@happyvertical/smrt-comments/svelte#RecordCommentsPlayground\\\",\\\"label\\\":\\\"Record discussion preview\\\"}],\\\"runtime\\\":\\\"both\\\",\\\"demoSeed\\\":{\\\"export\\\":\\\"@happyvertical/smrt-comments/svelte#recordCommentsDemo\\\"},\\\"demo\\\":{\\\"mode\\\":\\\"server\\\",\\\"reasons\\\":[\\\"Needs a server because its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624).\\\"]}}],\\\"smrtDependencies\\\":[\\\"@happyvertical/smrt-core\\\",\\\"@happyvertical/smrt-messages\\\",\\\"@happyvertical/smrt-tenancy\\\"],\\\"browser\\\":{\\\"status\\\":\\\"server-only\\\",\\\"issues\\\":[\\\"#3624\\\"],\\\"via\\\":[\\\"@happyvertical/smrt-tenancy\\\"]},\\\"stampsConstructors\\\":true}\"));\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_tenant_request_key',\n columns: ['retryTenantId', 'idempotencyKey'],\n unique: true,\n },\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 /** Service-owned binding retained after generic runOnce claim retention. */\n @field({ type: 'text', nullable: true, readonly: true })\n idempotencyKey: string | null = null;\n\n /** Canonical retry namespace; never rewrites stored tenant/user references. */\n @field({ type: 'text', nullable: true, readonly: true })\n retryTenantId: string | null = null;\n\n /** Digest of the original authenticated author, parent and normalized payload. */\n @field({ type: 'text', nullable: true, readonly: true })\n requestDigest: string | null = null;\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 if (options.idempotencyKey !== undefined)\n this.idempotencyKey = options.idempotencyKey;\n if (options.retryTenantId !== undefined)\n this.retryTenantId = options.retryTenantId;\n if (options.requestDigest !== undefined)\n this.requestDigest = options.requestDigest;\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 {\n digestRunOnceContent,\n runOnce,\n type SmrtClassOptions,\n} 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\n// Native UUID columns canonicalize case; identity and digests must agree with them.\nconst canonicalUuid = (value: string) =>\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)\n ? value.toLowerCase()\n : value;\n\n/** A reused request differs, or its retained comment is no longer available. */\nexport class CommentIdempotencyError extends Error {\n constructor(\n readonly code: 'COMMENT_REQUEST_CONFLICT' | 'COMMENT_REQUEST_UNAVAILABLE',\n ) {\n super(\n code === 'COMMENT_REQUEST_CONFLICT'\n ? 'Comment request key already used with different content.'\n : 'Comment request no longer has an unchanged comment.',\n );\n this.name = 'CommentIdempotencyError';\n }\n}\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 const tenantId = this.options.actor.tenantId;\n const binding = canonicalUuid(tenantId);\n const rows = await withTenant({ tenantId }, async () =>\n (await CommentCollection.create({ db: this.options.db })).listForRecord(\n recordType,\n recordId,\n ),\n );\n const collection = await withTenant({ tenantId }, () =>\n CommentCollection.create({ db: this.options.db }),\n );\n // The canonical retry namespace is separate from legacy reference spelling.\n // This query is tenant/parent-bound before any scoped hydration.\n const bound = await collection.db.query(\n `SELECT CAST(id AS TEXT) AS id, CAST(tenant_id AS TEXT) AS tenant_id FROM \"${collection.tableName.replace(/\"/g, '\"\"')}\" WHERE retry_tenant_id = ? AND meta_type = ? AND meta_id = ? AND idempotency_key IS NOT NULL`,\n binding,\n recordType,\n recordId,\n );\n for (const stored of bound.rows) {\n const row = await this.loadBoundComment(collection.db, stored, binding);\n if (row.metaType !== recordType || row.metaId !== recordId) {\n throw new CommentIdempotencyError('COMMENT_REQUEST_UNAVAILABLE');\n }\n rows.push(row);\n }\n for (const row of rows) {\n if (row.idempotencyKey !== null && row.retryTenantId !== binding) {\n throw new CommentIdempotencyError('COMMENT_REQUEST_UNAVAILABLE');\n }\n }\n return [...new Map(rows.map((row) => [row.id, row])).values()].sort(\n (a, b) =>\n (a.created_at?.getTime() ?? 0) - (b.created_at?.getTime() ?? 0) ||\n (a.id ?? '').localeCompare(b.id ?? ''),\n );\n }\n\n private async loadBoundComment(\n db: CommentCollection['db'],\n stored: { [key: string]: unknown },\n binding: string,\n ): Promise<Comment> {\n const id = stored.id;\n const tenantId = stored.tenant_id;\n if (\n typeof id !== 'string' ||\n typeof tenantId !== 'string' ||\n canonicalUuid(tenantId) !== binding\n ) {\n throw new CommentIdempotencyError('COMMENT_REQUEST_UNAVAILABLE');\n }\n // This is the original spelling of the same authorized tenant, not a bypass.\n const row = await withTenant({ tenantId }, async () =>\n (await CommentCollection.create({ db })).get(id),\n );\n if (\n !row ||\n canonicalUuid(row.tenantId) !== binding ||\n row.retryTenantId !== binding ||\n row.idempotencyKey === null\n ) {\n throw new CommentIdempotencyError('COMMENT_REQUEST_UNAVAILABLE');\n }\n return row;\n }\n\n async create(commentOptions: CreateCommentInput): Promise<Comment> {\n const {\n tenantId: inputTenantId,\n authorUserId: inputAuthorUserId,\n metaType,\n metaId,\n body,\n mentions,\n idempotencyKey,\n } = commentOptions;\n if (!inputTenantId || !inputAuthorUserId) {\n throw new TypeError('Comments require a tenant and author.');\n }\n if (\n canonicalUuid(inputTenantId) !==\n canonicalUuid(this.options.actor.tenantId) ||\n canonicalUuid(inputAuthorUserId) !==\n canonicalUuid(this.options.actor.userId)\n ) {\n throw new Error('Comment actor context mismatch.');\n }\n // Preserve the trusted context's stored spelling on text-backed databases.\n const tenantId = this.options.actor.tenantId;\n const authorUserId = this.options.actor.userId;\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 if (\n idempotencyKey !== undefined &&\n (typeof idempotencyKey !== 'string' ||\n !idempotencyKey.trim() ||\n idempotencyKey !== idempotencyKey.trim() ||\n idempotencyKey.length > 200)\n ) {\n throw new TypeError(\n 'Comment idempotencyKey must be a nonempty trimmed string of at most 200 characters.',\n );\n }\n const normalizedMentions = [...prepared.mentionUserIds].sort();\n const requestDigest = digestRunOnceContent({\n tenantId: canonicalUuid(tenantId),\n authorUserId: canonicalUuid(authorUserId),\n metaType,\n metaId,\n body,\n mentions: normalizedMentions,\n });\n const comment = await withTenant({ tenantId }, async () => {\n const collection = await CommentCollection.create({\n db: this.options.db,\n });\n const insert = async (target: CommentCollection) => {\n const id = crypto.randomUUID();\n return target.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 idempotencyKey: idempotencyKey ?? null,\n requestDigest: idempotencyKey === undefined ? null : requestDigest,\n retryTenantId:\n idempotencyKey === undefined ? null : canonicalUuid(tenantId),\n });\n };\n if (idempotencyKey === undefined) return insert(collection);\n // The internal actor is an arbitration namespace, NOT the authenticated\n // author. All authors sharing a tenant/key must contend on the same claim.\n // Actual author and payload are bound by the independently checked digest.\n const receipt = await runOnce(\n {\n db: collection.db,\n tenantId: canonicalUuid(tenantId),\n actor: 'smrt-comments.create:v1',\n token: idempotencyKey,\n content: 'comment-request-binding:v1',\n },\n async (tx) => {\n const scoped = await CommentCollection.create({ db: tx });\n const matches = await tx.query(\n `SELECT CAST(id AS TEXT) AS id, CAST(tenant_id AS TEXT) AS tenant_id FROM \"${scoped.tableName.replace(/\"/g, '\"\"')}\" WHERE retry_tenant_id = ? AND idempotency_key = ?`,\n canonicalUuid(tenantId),\n idempotencyKey,\n );\n if (matches.rows.length > 1) {\n throw new CommentIdempotencyError('COMMENT_REQUEST_UNAVAILABLE');\n }\n const row = matches.rows[0]\n ? await this.loadBoundComment(\n tx,\n matches.rows[0],\n canonicalUuid(tenantId),\n )\n : await insert(scoped);\n return {\n id: row.id,\n digest: row.requestDigest,\n tenant_id: row.tenantId,\n };\n },\n );\n if (receipt.digest !== requestDigest)\n throw new CommentIdempotencyError('COMMENT_REQUEST_CONFLICT');\n const row = await this.loadBoundComment(\n collection.db,\n receipt,\n canonicalUuid(tenantId),\n );\n if (\n !row ||\n canonicalUuid(row.tenantId) !== canonicalUuid(tenantId) ||\n row.idempotencyKey !== idempotencyKey ||\n row.requestDigest !== requestDigest ||\n digestRunOnceContent({\n tenantId: canonicalUuid(row.tenantId),\n authorUserId: canonicalUuid(row.authorUserId),\n metaType: row.metaType,\n metaId: row.metaId,\n body: row.body,\n mentions: [...row.mentionUserIds].sort(),\n }) !== requestDigest\n ) {\n throw new CommentIdempotencyError('COMMENT_REQUEST_UNAVAILABLE');\n }\n return row;\n });\n if (!this.options.mentionNotifications || !comment.id) return comment;\n for (const recipientUserId of comment.mentionUserIds) {\n if (\n canonicalUuid(recipientUserId) === canonicalUuid(comment.authorUserId)\n )\n 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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-comments",
|
|
5
|
-
"packageVersion": "0.
|
|
5
|
+
"packageVersion": "0.56.0",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-comments:CommentCollection": {
|
|
8
8
|
"name": "commentcollection",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"collectionExportName": "CommentCollectionCollection",
|
|
46
46
|
"schema": {
|
|
47
47
|
"tableName": "record_comments",
|
|
48
|
-
"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);",
|
|
48
|
+
"ddl": "CREATE TABLE IF NOT EXISTS \"record_comments\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"meta_type\" TEXT NOT NULL,\n \"meta_id\" TEXT NOT NULL,\n \"role\" TEXT NOT NULL,\n \"sort_order\" INTEGER DEFAULT 0,\n \"tenant_id\" UUID NOT NULL,\n \"author_user_id\" UUID NOT NULL,\n \"body\" TEXT NOT NULL,\n \"mentions\" TEXT,\n \"idempotency_key\" TEXT,\n \"retry_tenant_id\" TEXT,\n \"request_digest\" TEXT\n);",
|
|
49
49
|
"columns": {
|
|
50
50
|
"id": {
|
|
51
51
|
"type": "UUID",
|
|
@@ -114,6 +114,21 @@
|
|
|
114
114
|
"type": "TEXT",
|
|
115
115
|
"notNull": false,
|
|
116
116
|
"unique": false
|
|
117
|
+
},
|
|
118
|
+
"idempotency_key": {
|
|
119
|
+
"type": "TEXT",
|
|
120
|
+
"notNull": false,
|
|
121
|
+
"unique": false
|
|
122
|
+
},
|
|
123
|
+
"retry_tenant_id": {
|
|
124
|
+
"type": "TEXT",
|
|
125
|
+
"notNull": false,
|
|
126
|
+
"unique": false
|
|
127
|
+
},
|
|
128
|
+
"request_digest": {
|
|
129
|
+
"type": "TEXT",
|
|
130
|
+
"notNull": false,
|
|
131
|
+
"unique": false
|
|
117
132
|
}
|
|
118
133
|
},
|
|
119
134
|
"indexes": [
|
|
@@ -126,6 +141,14 @@
|
|
|
126
141
|
],
|
|
127
142
|
"unique": true
|
|
128
143
|
},
|
|
144
|
+
{
|
|
145
|
+
"name": "record_comments_tenant_request_key",
|
|
146
|
+
"columns": [
|
|
147
|
+
"retry_tenant_id",
|
|
148
|
+
"idempotency_key"
|
|
149
|
+
],
|
|
150
|
+
"unique": true
|
|
151
|
+
},
|
|
129
152
|
{
|
|
130
153
|
"name": "record_comments_record_created_idx",
|
|
131
154
|
"columns": [
|
|
@@ -156,7 +179,7 @@
|
|
|
156
179
|
]
|
|
157
180
|
}
|
|
158
181
|
],
|
|
159
|
-
"version": "
|
|
182
|
+
"version": "5095dfc9"
|
|
160
183
|
},
|
|
161
184
|
"importPath": "@happyvertical/smrt-comments"
|
|
162
185
|
},
|
|
@@ -226,6 +249,33 @@
|
|
|
226
249
|
"mentions": {
|
|
227
250
|
"type": "text",
|
|
228
251
|
"required": false
|
|
252
|
+
},
|
|
253
|
+
"idempotencyKey": {
|
|
254
|
+
"type": "text",
|
|
255
|
+
"required": false,
|
|
256
|
+
"_meta": {
|
|
257
|
+
"nullable": true,
|
|
258
|
+
"readonly": true
|
|
259
|
+
},
|
|
260
|
+
"readonly": true
|
|
261
|
+
},
|
|
262
|
+
"retryTenantId": {
|
|
263
|
+
"type": "text",
|
|
264
|
+
"required": false,
|
|
265
|
+
"_meta": {
|
|
266
|
+
"nullable": true,
|
|
267
|
+
"readonly": true
|
|
268
|
+
},
|
|
269
|
+
"readonly": true
|
|
270
|
+
},
|
|
271
|
+
"requestDigest": {
|
|
272
|
+
"type": "text",
|
|
273
|
+
"required": false,
|
|
274
|
+
"_meta": {
|
|
275
|
+
"nullable": true,
|
|
276
|
+
"readonly": true
|
|
277
|
+
},
|
|
278
|
+
"readonly": true
|
|
229
279
|
}
|
|
230
280
|
},
|
|
231
281
|
"methods": {
|
|
@@ -244,6 +294,14 @@
|
|
|
244
294
|
"mcp": false,
|
|
245
295
|
"cli": false,
|
|
246
296
|
"indexes": [
|
|
297
|
+
{
|
|
298
|
+
"name": "record_comments_tenant_request_key",
|
|
299
|
+
"columns": [
|
|
300
|
+
"retryTenantId",
|
|
301
|
+
"idempotencyKey"
|
|
302
|
+
],
|
|
303
|
+
"unique": true
|
|
304
|
+
},
|
|
247
305
|
{
|
|
248
306
|
"name": "record_comments_record_created_idx",
|
|
249
307
|
"columns": [
|
|
@@ -295,7 +353,7 @@
|
|
|
295
353
|
],
|
|
296
354
|
"schema": {
|
|
297
355
|
"tableName": "record_comments",
|
|
298
|
-
"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);",
|
|
356
|
+
"ddl": "CREATE TABLE IF NOT EXISTS \"record_comments\" (\n \"id\" UUID PRIMARY KEY NOT NULL,\n \"slug\" TEXT NOT NULL,\n \"context\" TEXT NOT NULL DEFAULT '',\n \"created_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"updated_at\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\n \"meta_type\" TEXT NOT NULL,\n \"meta_id\" TEXT NOT NULL,\n \"role\" TEXT NOT NULL,\n \"sort_order\" INTEGER DEFAULT 0,\n \"tenant_id\" UUID NOT NULL,\n \"author_user_id\" UUID NOT NULL,\n \"body\" TEXT NOT NULL,\n \"mentions\" TEXT,\n \"idempotency_key\" TEXT,\n \"retry_tenant_id\" TEXT,\n \"request_digest\" TEXT\n);",
|
|
299
357
|
"columns": {
|
|
300
358
|
"id": {
|
|
301
359
|
"type": "UUID",
|
|
@@ -364,6 +422,21 @@
|
|
|
364
422
|
"type": "TEXT",
|
|
365
423
|
"notNull": false,
|
|
366
424
|
"unique": false
|
|
425
|
+
},
|
|
426
|
+
"idempotency_key": {
|
|
427
|
+
"type": "TEXT",
|
|
428
|
+
"notNull": false,
|
|
429
|
+
"unique": false
|
|
430
|
+
},
|
|
431
|
+
"retry_tenant_id": {
|
|
432
|
+
"type": "TEXT",
|
|
433
|
+
"notNull": false,
|
|
434
|
+
"unique": false
|
|
435
|
+
},
|
|
436
|
+
"request_digest": {
|
|
437
|
+
"type": "TEXT",
|
|
438
|
+
"notNull": false,
|
|
439
|
+
"unique": false
|
|
367
440
|
}
|
|
368
441
|
},
|
|
369
442
|
"indexes": [
|
|
@@ -376,6 +449,14 @@
|
|
|
376
449
|
],
|
|
377
450
|
"unique": true
|
|
378
451
|
},
|
|
452
|
+
{
|
|
453
|
+
"name": "record_comments_tenant_request_key",
|
|
454
|
+
"columns": [
|
|
455
|
+
"retry_tenant_id",
|
|
456
|
+
"idempotency_key"
|
|
457
|
+
],
|
|
458
|
+
"unique": true
|
|
459
|
+
},
|
|
379
460
|
{
|
|
380
461
|
"name": "record_comments_record_created_idx",
|
|
381
462
|
"columns": [
|
|
@@ -406,7 +487,7 @@
|
|
|
406
487
|
]
|
|
407
488
|
}
|
|
408
489
|
],
|
|
409
|
-
"version": "
|
|
490
|
+
"version": "5095dfc9"
|
|
410
491
|
},
|
|
411
492
|
"importPath": "@happyvertical/smrt-comments"
|
|
412
493
|
}
|
package/dist/models/Comment.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare class Comment extends SmrtPolymorphicAssociation {
|
|
|
6
6
|
body: string;
|
|
7
7
|
/** JSON array of referenced User UUIDs; use mentionUserIds for validation. */
|
|
8
8
|
mentions: string;
|
|
9
|
+
/** Service-owned binding retained after generic runOnce claim retention. */
|
|
10
|
+
idempotencyKey: string | null;
|
|
11
|
+
/** Canonical retry namespace; never rewrites stored tenant/user references. */
|
|
12
|
+
retryTenantId: string | null;
|
|
13
|
+
/** Digest of the original authenticated author, parent and normalized payload. */
|
|
14
|
+
requestDigest: string | null;
|
|
9
15
|
constructor(options?: CommentOptions);
|
|
10
16
|
get mentionUserIds(): string[];
|
|
11
17
|
set mentionUserIds(value: readonly string[]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Comment.d.ts","sourceRoot":"","sources":["../../src/models/Comment.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,0BAA0B,EAE3B,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,
|
|
1
|
+
{"version":3,"file":"Comment.d.ts","sourceRoot":"","sources":["../../src/models/Comment.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,0BAA0B,EAE3B,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKlD,qBAkBa,OAAQ,SAAQ,0BAA0B;IAErD,QAAQ,SAAM;IAGd,YAAY,SAAM;IAGlB,IAAI,SAAM;IAEV,8EAA8E;IAC9E,QAAQ,SAAQ;IAEhB,4EAA4E;IAE5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAQ;IAErC,+EAA+E;IAE/E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEpC,kFAAkF;IAElF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAExB,OAAO,GAAE,cAAmB;IAiBxC,IAAI,cAAc,IAAI,MAAM,EAAE,CAU7B;IAED,IAAI,cAAc,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAQ1C;CACF"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SmrtClassOptions } from '@happyvertical/smrt-core';
|
|
2
2
|
import { Comment } from '../models/Comment.js';
|
|
3
3
|
import type { CommentActorContext, CommentMentionNotificationAdapter, CommentRecordAccess, CreateCommentInput } from '../types.js';
|
|
4
|
+
/** A reused request differs, or its retained comment is no longer available. */
|
|
5
|
+
export declare class CommentIdempotencyError extends Error {
|
|
6
|
+
readonly code: 'COMMENT_REQUEST_CONFLICT' | 'COMMENT_REQUEST_UNAVAILABLE';
|
|
7
|
+
constructor(code: 'COMMENT_REQUEST_CONFLICT' | 'COMMENT_REQUEST_UNAVAILABLE');
|
|
8
|
+
}
|
|
4
9
|
export declare class CommentService {
|
|
5
10
|
private readonly options;
|
|
6
11
|
constructor(options: {
|
|
@@ -11,6 +16,7 @@ export declare class CommentService {
|
|
|
11
16
|
});
|
|
12
17
|
private authorize;
|
|
13
18
|
listForRecord(recordType: string, recordId: string): Promise<Comment[]>;
|
|
19
|
+
private loadBoundComment;
|
|
14
20
|
create(commentOptions: CreateCommentInput): Promise<Comment>;
|
|
15
21
|
}
|
|
16
22
|
//# 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,
|
|
1
|
+
{"version":3,"file":"CommentService.d.ts","sourceRoot":"","sources":["../../src/services/CommentService.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,KAAK,EACV,mBAAmB,EACnB,iCAAiC,EACjC,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAQrB,gFAAgF;AAChF,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,QAAQ,CAAC,IAAI,EAAE,0BAA0B,GAAG,6BAA6B;gBAAhE,IAAI,EAAE,0BAA0B,GAAG,6BAA6B;CAS5E;AAED,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;YAwCP,gBAAgB;IA6BxB,MAAM,CAAC,cAAc,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;CAwJnE"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-comments",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.56.0",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
13
|
-
"moduleDoc:agents/test-design.md": "
|
|
10
|
+
"manifest": "2bfcce146b3d36b5d4d4a10b64e7b33d1abd240c7d38a1dcef3241a4b8427afe",
|
|
11
|
+
"packageJson": "a1d9db8367441f23c1b6c53da102fe846dc5423c27cb7eee4e3e597755c44307",
|
|
12
|
+
"agents": "3e82791ea4a860351e1be18ee8cee07902b99643bd9c53d4859e1f18683979ec",
|
|
13
|
+
"moduleDoc:agents/test-design.md": "df07180c4186d6358a841e23c806cd521f772a98582676e7f0ab73035d5cbb53",
|
|
14
|
+
"moduleDoc:agents/idempotent-create.md": "97043e654f93fe59f1c2bbc2f5f3adbfad616e2741756c69fa5ca7ff6d84250f"
|
|
14
15
|
},
|
|
15
16
|
"exports": [
|
|
16
17
|
".",
|
|
@@ -145,6 +146,27 @@
|
|
|
145
146
|
"type": "text",
|
|
146
147
|
"required": false,
|
|
147
148
|
"columnType": "TEXT"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "idempotencyKey",
|
|
152
|
+
"type": "text",
|
|
153
|
+
"required": false,
|
|
154
|
+
"columnType": "TEXT",
|
|
155
|
+
"readonly": true
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "retryTenantId",
|
|
159
|
+
"type": "text",
|
|
160
|
+
"required": false,
|
|
161
|
+
"columnType": "TEXT",
|
|
162
|
+
"readonly": true
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "requestDigest",
|
|
166
|
+
"type": "text",
|
|
167
|
+
"required": false,
|
|
168
|
+
"columnType": "TEXT",
|
|
169
|
+
"readonly": true
|
|
148
170
|
}
|
|
149
171
|
],
|
|
150
172
|
"relationships": [
|
|
@@ -205,12 +227,17 @@
|
|
|
205
227
|
"polymorphicAssociations": 1,
|
|
206
228
|
"uuidColumns": 6
|
|
207
229
|
},
|
|
208
|
-
"agentDoc": "# @happyvertical/smrt-comments\n\nRecord discussions attach to arbitrary records through `Comment`'s inherited\n`metaType` and `metaId`; preserve these as a qualified polymorphic pair and\nnever replace them with a text foreign key. `CommentService` owns persistence\nand invokes the optional mention adapter only after the comment is saved.\n\nThe package must not depend on `smrt-messages`: hosts compose\n`CommentMentionNotificationAdapter` with `UserNotificationService` and retain\ntheir own notification delivery policy. The Svelte surface receives authorized\ncomment projections and callbacks; it does not fetch or authorize records.\n`CommentService` requires a trusted actor and host record authorization callback.\n`RecordComments.contextKey` includes tenant, actor and record identity.\nSee [test design](agents/test-design.md) for
|
|
230
|
+
"agentDoc": "# @happyvertical/smrt-comments\n\nRecord discussions attach to arbitrary records through `Comment`'s inherited\n`metaType` and `metaId`; preserve these as a qualified polymorphic pair and\nnever replace them with a text foreign key. `CommentService` owns persistence\nand invokes the optional mention adapter only after the comment is saved.\n\nThe package must not depend on `smrt-messages`: hosts compose\n`CommentMentionNotificationAdapter` with `UserNotificationService` and retain\ntheir own notification delivery policy. The Svelte surface receives authorized\ncomment projections and callbacks; it does not fetch or authorize records.\n`CommentService` requires a trusted actor and host record authorization callback.\n`RecordComments.contextKey` includes tenant, actor and record identity.\nSee [test design](agents/test-design.md) and [idempotent creation](agents/idempotent-create.md) for behavior matrices and retry invariants.\n\nRun `pnpm --filter @happyvertical/smrt-comments test`, `test:postgres`, `typecheck`, `check`, and\n`npx --yes @biomejs/biome@2.5.2 check packages/comments` after changes.\n",
|
|
209
231
|
"moduleDocs": [
|
|
210
232
|
{
|
|
211
233
|
"path": "agents/test-design.md",
|
|
212
234
|
"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.
|
|
235
|
+
"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. Unkeyed creates have no retry contract. Explicit keyed creates now support\nretry recovery; see [idempotent creation](idempotent-create.md).\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"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"path": "agents/idempotent-create.md",
|
|
239
|
+
"module": "idempotent-create",
|
|
240
|
+
"content": "# Idempotent comment creation (#3941)\n\n`CommentService.create({...input, idempotencyKey})` returns a `Comment`, including\nits server-generated UUID. Keys are optional, trimmed nonempty strings of at most\n200 characters, unique within a tenant. Omitted keys retain the original create\nbehavior. The service allowlists input: identity, slug, context, executor and\nrequestDigest and retryTenantId cannot be supplied by the caller.\n\nValid UUID tenant/author identities use lowercase for equality, arbitration and\ndigest comparisons. Authorization and stored tenant/author references preserve the\ntrusted actor spelling, including existing SQLite identities. A separate nullable\n`retryTenantId` binds keyed rows to the canonical tenant; its unique index with\n`idempotencyKey` matches arbitration and retention lookup. Opaque textual tenant\nIDs remain case-sensitive, and parent record keys remain exact.\nThe trusted actor and record authorization run before every attempt, including\nreplays. A key binds the actual author, qualified parent, exact body and sorted,\ndeduplicated validated mentions. Reusing a key with different bound content throws\n`CommentIdempotencyError` with code `COMMENT_REQUEST_CONFLICT`. Replays return the\noriginal unchanged comment. A retained receipt referring to a missing or modified\ncomment throws `COMMENT_REQUEST_UNAVAILABLE`, never recreates it.\n\nThe service reuses core `runOnce` for atomic arbitration. Its fixed internal actor\n`smrt-comments.create:v1` is only an arbitration namespace: different authenticated\nauthors racing the same tenant/key must contend together. The actual author is\nbound in the separate immutable request digest. Claim insertion uses core's\nconflict-safe SQL; comment insertion uses the callback's executor and remains\ninsert-only with a random server UUID. Nullable key/digest/retry-tenant fields on Comment retain the binding after normal\ngeneric claim retention. Existing rows need no key or canonical binding.\n\nInternal lookups use parameterized queries against the resolved public\n`CommentCollection.tableName`, honoring `smrt.tableNames` deployment bindings.\nThe table identifier is quoted separately from bound values (mapping configuration\naccepts a single lowercase SQL identifier, not schema-qualified names). Lookups are\nrestricted to canonical retry tenant plus key (or exact authorized parent for\nlisting). The stored tenant must be UUID-equivalent to the authorized tenant before\nhydration in that original tenant scope; no system-context bypass is used. Lists\nmerge legacy exact-scope rows with equivalent-tenant keyed rows, deduplicate by\nserver UUID and sort by public creation timestamp with UUID as a tie-breaker. This\npreserves old unkeyed listing semantics while showing keyed replies across trusted\nactor UUID spellings. Stored tenant and author references are never rewritten.\n\nThe caller's DB must support transactions for keyed creates. Nested transaction\nsupport follows the database adapter: unsupported nested transactions fail before\nwork, rather than silently widening the supplied executor. Hosts needing atomic\ncomment/notification rollback compose the service and notification adapter with\nthe same outer transaction handle. Notification delivery happens after comment\narbitration, including on identical retries. The default messages adapter's stable\ncomment sourceRef deduplicates delivered mentions and retries missing delivery.\nArbitrary adapters must provide their own replay safety; no exactly-once external\ndelivery is promised. Without an outer transaction a delivery failure leaves the\ncomment saved, and retrying the same key is supported.\n\nThe service exposes no edit/delete lifecycle. Trusted model/collection bypasses\nmust preserve key/digest binding. Deleting both the comment and retained core\nclaim intentionally destroys retry history and is outside this contract. Do not\npurge retry history while clients may still retry. Runtime does not create schema;\napply normal SMRT migrations before using keyed creates.\n\n## Test-design contract\n\nAll rows use the authenticated author and explicit tenant context, a real supplied\nDB executor, and SQLite/DuckDB/PostgreSQL unless noted. Commands are package `test`\nand `test:postgres`; public TypeScript contract is also checked by `typecheck`.\n\n| Invariant / reachable trigger | Positive | Negative / failure | Executor / edge / level |\n| --- | --- | --- | --- |\n| Optional keyed create and retry | Same key and normalized mentions return one UUID | Empty, oversized or nonstring keys rejected before mutation | Root DB, public input, integration |\n| Immutable tenant/request binding | Different tenants may reuse key | Changed author, parent, body or mentions conflicts | Authenticated actor, public service, integration |\n| Authorization precedes replay | Authorized replay succeeds | Revoked record access or forged tenant/actor denied | Same DB, host authorization callback, integration |\n| Concurrent arbitration | Simultaneous identical creates produce one row | Competing authors/payloads yield one winner, one typed conflict | Root DB; PostgreSQL outer transactions also remain usable, integration |\n| Transaction affinity | Comment and claim use runOnce callback executor | Caller rollback removes comment, claim and composed notifications | PostgreSQL/SQLite supported nesting; DuckDB rejects unsupported nesting before writes, integration |\n| Delivery recovery | Default notifications replay once per recipient | Partial adapter failure retains comment; same-key retry finishes missing notification | Real messages adapter and supplied DB; integration |\n| Durable binding | All 16 actor/input UUID casing combinations, before/after claim retention, reuse one comment | Deleted/modified retained comment or forged canonical tenant binding is unavailable | Cold concurrent cross-case actors, unique index independent of claim, all dialects |\n| Legacy references/listing | Uppercase Tenant/User references and legacy NULL-binding rows remain usable | Another tenant/parent and differently cased opaque IDs stay isolated | Mixed legacy/keyed order and dedup; old-to-new migration repeat |\n| Persistence-option injection | Generated UUID remains server-owned | Injected id/slug/context/db/digest cannot replace another row | Existing three-dialect regression suite plus keyed case |\n\nNew additive API: baseline rejects `idempotencyKey` at the TypeScript boundary and\nignores it at runtime, creating duplicate rows. Record an executable baseline\nretry failure before final evidence. Browser behavior is unchanged; existing Svelte\nchecks/tests remain required, and a new browser flow is N/A.\n"
|
|
214
241
|
}
|
|
215
242
|
],
|
|
216
243
|
"recipes": [
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export interface CommentOptions extends SmrtPolymorphicAssociationOptions {
|
|
|
4
4
|
authorUserId?: string;
|
|
5
5
|
body?: string;
|
|
6
6
|
mentions?: readonly string[];
|
|
7
|
+
idempotencyKey?: string | null;
|
|
8
|
+
requestDigest?: string | null;
|
|
9
|
+
retryTenantId?: string | null;
|
|
7
10
|
}
|
|
8
11
|
/** Public service input; persistence identity and executor are server-owned. */
|
|
9
12
|
export interface CreateCommentInput {
|
|
@@ -13,6 +16,8 @@ export interface CreateCommentInput {
|
|
|
13
16
|
metaId: string;
|
|
14
17
|
body: string;
|
|
15
18
|
mentions?: readonly string[];
|
|
19
|
+
/** Tenant-scoped request key; identical retries return the original comment. */
|
|
20
|
+
idempotencyKey?: string;
|
|
16
21
|
}
|
|
17
22
|
export interface CommentMention {
|
|
18
23
|
commentId: 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;
|
|
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;IAC7B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;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;IAC7B,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;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.
|
|
3
|
+
"version": "0.56.0",
|
|
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.
|
|
28
|
-
"@happyvertical/smrt-tenancy": "0.
|
|
27
|
+
"@happyvertical/smrt-core": "0.56.0",
|
|
28
|
+
"@happyvertical/smrt-tenancy": "0.56.0"
|
|
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.
|
|
46
|
+
"@happyvertical/smrt-vitest": "0.56.0",
|
|
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.
|
|
56
|
+
"@happyvertical/smrt-messages": "0.56.0"
|
|
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",
|