@basaltkit/comments adiciona comentários em thread a qualquer recurso — uma nota, um projeto, uma tarefa — com @mentions e resolve/reopen, delimitados por tenant. Emite eventos que fazem ponte de forma limpa com o realtime (discussão ao vivo) e as notificações (alertar os mencionados).
Regista commentsPlugin e monta as rotas REST já prontas através do teu adaptador. Em dev o store é em memória; em produção passa um store suportado por @basaltkit/comments-prisma ou -sqlite:
add extrai as mentions com @([\w-]+) por defeito. Passa mentionPattern (uma regex cujo primeiro grupo de captura é o user id) ao commentsPlugin para corresponder ao teu próprio esquema de ids.
add extrai as @mentions do corpo (padrão configurável), armazena-as no comentário, e emite comment:created mais um comment:mentioned por cada utilizador mencionado. Também: edit, remove, resolve(id, by), reopen(id).
Corpos e mentions limitados
Um corpo maior que maxBodyLength (predefinição 10 000 caracteres) lança CommentTooLongError (400 COMMENT_TOO_LONG), e um com mais de maxMentions mentions distintas (predefinição 50) lança CommentMentionLimitError (400 COMMENT_TOO_MANY_MENTIONS) — em add e em edit, antes de guardar ou emitir o que quer que seja. De resto cada @id é aceite tal como vem, por isso quando comment:mentioned chega a um canal de notificação real, passa resolveMentions(ids, tenantId) para manter só os utilizadores que podem ser mencionados — tipicamente os membros do tenant:
Dentro de um contexto de tenant, um argumento tenantId explícito tem de nomear esse tenant; qualquer outro valor lança CommentTenantMismatchError (403 COMMENT_TENANT_MISMATCH). Só escolhe um tenant fora de um (jobs, CLI).
Numa app single-tenant — sem tenancyPlugin — as chamadas não precisam de tenantId, e os comentários ficam numa única chave interna, SINGLE_TENANT_SCOPE ('@single' — fora da gramática de ids de tenant, logo nenhum tenant pode receber esses comentários). Um id de tenant igual a ela é recusado com CommentTenantReservedError (400 COMMENT_TENANT_RESERVED).
Atualizar dados single-tenant
Antes do @basaltkit/comments 4.0 a chave single-tenant era 'default' — um id de tenant válido, logo um tenant chamado default lia, editava e apagava os comentários single-tenant. Uma app single-tenant com comentários persistidos muda-lhes a chave uma vez: UPDATE comments SET "tenantId" = '@single' WHERE "tenantId" = 'default' (@basaltkit/comments-prisma; no @basaltkit/comments-sqlite a coluna é tenant_id). Salta este passo se default alguma vez foi um tenant real nessa base de dados.
Cada mutação emite um hook (comment:created, comment:mentioned, comment:updated, comment:deleted, comment:resolved, comment:reopened), por isso atualizações ao vivo e notificações ligam-se sem acoplamento. Subscreve em app.hooks:
ts
import { REALTIME } from '@basaltkit/realtime'import { NOTIFIER, defineNotification } from '@basaltkit/notifications'import { z } from 'zod'import { app } from './app.js'const realtime = app.container.get(REALTIME)const notifier = app.container.get(NOTIFIER)const CommentMention = defineNotification({ name: 'comment.mention', schema: z.object({ by: z.string() }), channels: ['inApp'], via: { inApp: ({ by }) => ({ title: 'You were mentioned', data: { by } }) },})// push new comments to everyone viewing the resourceapp.hooks.on('comment:created', ({ comment }) => realtime .to(comment.tenantId) .channel(`${comment.resourceType}:${comment.resourceId}`) .emit('comment', comment))// notify the mentioned — one hook fires per mentioned userapp.hooks.on('comment:mentioned', ({ comment, userId }) => notifier.notify({ id: userId }, CommentMention, { by: comment.authorId }))
commentRoutes() (exigem um utilizador autenticado; o autor é tirado de ctx().user): GET /comments?resourceType=&resourceId=, POST /comments, PATCH /comments/:id, DELETE /comments/:id, POST /comments/:id/resolve e /reopen. Por predefinição qualquer utilizador do tenant pode ler uma thread e publicar nela, e editar, apagar, resolver e reabrir estão restritos ao autor do comentário (403 COMMENT_FORBIDDEN). Tudo é delimitado por tenant.
Passa authorize para ligar uma thread às regras de acesso do recurso que discute. Substitui a política predefinida; compõe com defaultCommentPolicy para a manter:
Comentários
@basaltkit/commentsadiciona comentários em thread a qualquer recurso — uma nota, um projeto, uma tarefa — com @mentions e resolve/reopen, delimitados por tenant. Emite eventos que fazem ponte de forma limpa com o realtime (discussão ao vivo) e as notificações (alertar os mencionados).Setup
Regista
commentsPlugine monta as rotas REST já prontas através do teu adaptador. Em dev o store é em memória; em produção passa umstoresuportado por@basaltkit/comments-prismaou-sqlite:Dica
addextrai as mentions com@([\w-]+)por defeito. PassamentionPattern(uma regex cujo primeiro grupo de captura é o user id) aocommentsPluginpara corresponder ao teu próprio esquema de ids.Adicionar e ler
addextrai as @mentions do corpo (padrão configurável), armazena-as no comentário, e emitecomment:createdmais umcomment:mentionedpor cada utilizador mencionado. Também:edit,remove,resolve(id, by),reopen(id).Corpos e mentions limitados
Um corpo maior que
maxBodyLength(predefinição 10 000 caracteres) lançaCommentTooLongError(400 COMMENT_TOO_LONG), e um com mais demaxMentionsmentions distintas (predefinição 50) lançaCommentMentionLimitError(400 COMMENT_TOO_MANY_MENTIONS) — emadde emedit, antes de guardar ou emitir o que quer que seja. De resto cada@idé aceite tal como vem, por isso quandocomment:mentionedchega a um canal de notificação real, passaresolveMentions(ids, tenantId)para manter só os utilizadores que podem ser mencionados — tipicamente os membros do tenant:Dentro de um contexto de tenant, um argumento
tenantIdexplícito tem de nomear esse tenant; qualquer outro valor lançaCommentTenantMismatchError(403 COMMENT_TENANT_MISMATCH). Só escolhe um tenant fora de um (jobs, CLI).Numa app single-tenant — sem
tenancyPlugin— as chamadas não precisam detenantId, e os comentários ficam numa única chave interna,SINGLE_TENANT_SCOPE('@single'— fora da gramática de ids de tenant, logo nenhum tenant pode receber esses comentários). Um id de tenant igual a ela é recusado comCommentTenantReservedError(400 COMMENT_TENANT_RESERVED).Atualizar dados single-tenant
Antes do
@basaltkit/comments4.0 a chave single-tenant era'default'— um id de tenant válido, logo um tenant chamadodefaultlia, editava e apagava os comentários single-tenant. Uma app single-tenant com comentários persistidos muda-lhes a chave uma vez:UPDATE comments SET "tenantId" = '@single' WHERE "tenantId" = 'default'(@basaltkit/comments-prisma; no@basaltkit/comments-sqlitea coluna étenant_id). Salta este passo sedefaultalguma vez foi um tenant real nessa base de dados.Discussão ao vivo + notificações de mention
Cada mutação emite um hook (
comment:created,comment:mentioned,comment:updated,comment:deleted,comment:resolved,comment:reopened), por isso atualizações ao vivo e notificações ligam-se sem acoplamento. Subscreve emapp.hooks:Rotas
commentRoutes()(exigem um utilizador autenticado; o autor é tirado dectx().user):GET /comments?resourceType=&resourceId=,POST /comments,PATCH /comments/:id,DELETE /comments/:id,POST /comments/:id/resolvee/reopen. Por predefinição qualquer utilizador do tenant pode ler uma thread e publicar nela, e editar, apagar, resolver e reabrir estão restritos ao autor do comentário (403 COMMENT_FORBIDDEN). Tudo é delimitado por tenant.Passa
authorizepara ligar uma thread às regras de acesso do recurso que discute. Substitui a política predefinida; compõe comdefaultCommentPolicypara a manter:Aqui não é preciso UI já pronta — os comentários renderizam inline na tua app — mas o mesmo padrão autocontido alimenta o visualizador de auditoria.