@actuate-media/cms-core 0.21.0 → 0.22.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/dist/__tests__/actions/document-crud.test.js +74 -2
- package/dist/__tests__/actions/document-crud.test.js.map +1 -1
- package/dist/__tests__/api/cache-headers.test.d.ts +2 -0
- package/dist/__tests__/api/cache-headers.test.d.ts.map +1 -0
- package/dist/__tests__/api/cache-headers.test.js +135 -0
- package/dist/__tests__/api/cache-headers.test.js.map +1 -0
- package/dist/__tests__/api/comments-routes.test.d.ts +2 -0
- package/dist/__tests__/api/comments-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/comments-routes.test.js +266 -0
- package/dist/__tests__/api/comments-routes.test.js.map +1 -0
- package/dist/__tests__/api/notifications-routes.test.d.ts +2 -0
- package/dist/__tests__/api/notifications-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/notifications-routes.test.js +225 -0
- package/dist/__tests__/api/notifications-routes.test.js.map +1 -0
- package/dist/__tests__/api/preview-and-scheduling.test.js +53 -0
- package/dist/__tests__/api/preview-and-scheduling.test.js.map +1 -1
- package/dist/__tests__/fields/relations.test.js +62 -8
- package/dist/__tests__/fields/relations.test.js.map +1 -1
- package/dist/__tests__/preview-event-bus.test.d.ts +2 -0
- package/dist/__tests__/preview-event-bus.test.d.ts.map +1 -0
- package/dist/__tests__/preview-event-bus.test.js +89 -0
- package/dist/__tests__/preview-event-bus.test.js.map +1 -0
- package/dist/__tests__/preview-sse-stream.test.d.ts +2 -0
- package/dist/__tests__/preview-sse-stream.test.d.ts.map +1 -0
- package/dist/__tests__/preview-sse-stream.test.js +117 -0
- package/dist/__tests__/preview-sse-stream.test.js.map +1 -0
- package/dist/__tests__/realtime/comments.test.d.ts +2 -0
- package/dist/__tests__/realtime/comments.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/comments.test.js +508 -0
- package/dist/__tests__/realtime/comments.test.js.map +1 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts +2 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.js +170 -0
- package/dist/__tests__/realtime/gateway-ws-integration.test.js.map +1 -0
- package/dist/__tests__/realtime/gateway.test.d.ts +2 -0
- package/dist/__tests__/realtime/gateway.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/gateway.test.js +402 -0
- package/dist/__tests__/realtime/gateway.test.js.map +1 -0
- package/dist/__tests__/realtime/notifications.test.d.ts +2 -0
- package/dist/__tests__/realtime/notifications.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/notifications.test.js +317 -0
- package/dist/__tests__/realtime/notifications.test.js.map +1 -0
- package/dist/__tests__/realtime/persistence.test.d.ts +2 -0
- package/dist/__tests__/realtime/persistence.test.d.ts.map +1 -0
- package/dist/__tests__/realtime/persistence.test.js +124 -0
- package/dist/__tests__/realtime/persistence.test.js.map +1 -0
- package/dist/__tests__/schema-preview-features.test.d.ts +2 -0
- package/dist/__tests__/schema-preview-features.test.d.ts.map +1 -0
- package/dist/__tests__/schema-preview-features.test.js +42 -0
- package/dist/__tests__/schema-preview-features.test.js.map +1 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +71 -26
- package/dist/actions.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +734 -188
- package/dist/api/handlers.js.map +1 -1
- package/dist/cache/__tests__/http.test.d.ts +2 -0
- package/dist/cache/__tests__/http.test.d.ts.map +1 -0
- package/dist/cache/__tests__/http.test.js +285 -0
- package/dist/cache/__tests__/http.test.js.map +1 -0
- package/dist/cache/http.d.ts +119 -0
- package/dist/cache/http.d.ts.map +1 -0
- package/dist/cache/http.js +188 -0
- package/dist/cache/http.js.map +1 -0
- package/dist/config/types.d.ts +92 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/fields/relations.d.ts.map +1 -1
- package/dist/fields/relations.js +43 -11
- package/dist/fields/relations.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/page-builder/schema.d.ts +2 -2
- package/dist/preview/event-bus.d.ts +39 -0
- package/dist/preview/event-bus.d.ts.map +1 -0
- package/dist/preview/event-bus.js +73 -0
- package/dist/preview/event-bus.js.map +1 -0
- package/dist/preview/sse-stream.d.ts +50 -0
- package/dist/preview/sse-stream.d.ts.map +1 -0
- package/dist/preview/sse-stream.js +120 -0
- package/dist/preview/sse-stream.js.map +1 -0
- package/dist/realtime/comments.d.ts +217 -0
- package/dist/realtime/comments.d.ts.map +1 -0
- package/dist/realtime/comments.js +282 -0
- package/dist/realtime/comments.js.map +1 -0
- package/dist/realtime/gateway.d.ts +107 -0
- package/dist/realtime/gateway.d.ts.map +1 -0
- package/dist/realtime/gateway.js +215 -0
- package/dist/realtime/gateway.js.map +1 -0
- package/dist/realtime/index.d.ts +18 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +18 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/notifications.d.ts +209 -0
- package/dist/realtime/notifications.d.ts.map +1 -0
- package/dist/realtime/notifications.js +209 -0
- package/dist/realtime/notifications.js.map +1 -0
- package/dist/realtime/persistence.d.ts +72 -0
- package/dist/realtime/persistence.d.ts.map +1 -0
- package/dist/realtime/persistence.js +53 -0
- package/dist/realtime/persistence.js.map +1 -0
- package/package.json +13 -4
- package/prisma/cms-schema.prisma +135 -4
- package/prisma/schema.prisma +17 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { subscribePreviewUpdates } from './event-bus.js';
|
|
2
|
+
/**
|
|
3
|
+
* SSE wire format helpers. We keep them small and pure so the route handler
|
|
4
|
+
* can build a `ReadableStream<Uint8Array>` without pulling in any extra
|
|
5
|
+
* dependencies (this also keeps the file edge-compatible).
|
|
6
|
+
*/
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
/**
|
|
9
|
+
* Default keepalive interval. 25 s sits safely under the most aggressive
|
|
10
|
+
* proxy/idle timeouts we've encountered in production (Cloudflare 30 s,
|
|
11
|
+
* Vercel Serverless 30 s, most reverse-proxies 60 s).
|
|
12
|
+
*/
|
|
13
|
+
export const PREVIEW_SSE_HEARTBEAT_MS = 25_000;
|
|
14
|
+
/**
|
|
15
|
+
* Maximum stream lifetime in ms. After this, the server closes the stream
|
|
16
|
+
* cleanly so EventSource will reconnect — important because some platforms
|
|
17
|
+
* (Vercel Fluid Compute, Cloudflare Workers) cap any single response at a
|
|
18
|
+
* few minutes regardless of activity.
|
|
19
|
+
*/
|
|
20
|
+
export const PREVIEW_SSE_MAX_LIFETIME_MS = 4 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* Encode a single SSE message. `event` is optional; when omitted the
|
|
23
|
+
* default `message` event is used.
|
|
24
|
+
*/
|
|
25
|
+
export function encodeSseEvent(data, event) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
if (event)
|
|
28
|
+
lines.push(`event: ${event}`);
|
|
29
|
+
const serialized = typeof data === 'string' ? data : JSON.stringify(data);
|
|
30
|
+
for (const line of serialized.split('\n')) {
|
|
31
|
+
lines.push(`data: ${line}`);
|
|
32
|
+
}
|
|
33
|
+
lines.push('');
|
|
34
|
+
lines.push('');
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a `ReadableStream` of Server-Sent Events for the given preview
|
|
39
|
+
* session. The stream:
|
|
40
|
+
*
|
|
41
|
+
* 1. Emits an `event: ready` frame with the session's collection/id.
|
|
42
|
+
* Lets the client confirm the server-side subscription is wired up
|
|
43
|
+
* before it starts treating the absence of events as "stale".
|
|
44
|
+
* 2. Forwards every `notifyPreviewUpdate(collection, id, payload)` to
|
|
45
|
+
* the client as `event: update` with `payload` as JSON data.
|
|
46
|
+
* 3. Sends `: heartbeat\n\n` keepalive comments on
|
|
47
|
+
* `PREVIEW_SSE_HEARTBEAT_MS` intervals.
|
|
48
|
+
* 4. Cleanly closes after `PREVIEW_SSE_MAX_LIFETIME_MS` and lets the
|
|
49
|
+
* browser reconnect, sidestepping platform idle/max-duration caps.
|
|
50
|
+
*
|
|
51
|
+
* The returned stream MUST be wrapped in a `Response` with the SSE
|
|
52
|
+
* headers — `createPreviewStreamResponse` does this for typical callers.
|
|
53
|
+
*/
|
|
54
|
+
export function createPreviewStream(session, opts = {}) {
|
|
55
|
+
const heartbeatMs = opts.heartbeatMs ?? PREVIEW_SSE_HEARTBEAT_MS;
|
|
56
|
+
const maxLifetimeMs = opts.maxLifetimeMs ?? PREVIEW_SSE_MAX_LIFETIME_MS;
|
|
57
|
+
let heartbeat;
|
|
58
|
+
let lifetime;
|
|
59
|
+
let unsubscribe;
|
|
60
|
+
return new ReadableStream({
|
|
61
|
+
start(controller) {
|
|
62
|
+
const enqueue = (chunk) => {
|
|
63
|
+
try {
|
|
64
|
+
controller.enqueue(encoder.encode(chunk));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Controller can be closed mid-emit if the client disconnected
|
|
68
|
+
// between the `notifyPreviewUpdate` call and the enqueue. Swallow.
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
enqueue(encodeSseEvent({ collection: session.collection, documentId: session.documentId }, 'ready'));
|
|
72
|
+
unsubscribe = subscribePreviewUpdates(session.collection, session.documentId, (event) => {
|
|
73
|
+
enqueue(encodeSseEvent({
|
|
74
|
+
collection: event.collection,
|
|
75
|
+
documentId: event.documentId,
|
|
76
|
+
at: event.at,
|
|
77
|
+
...event.payload,
|
|
78
|
+
}, 'update'));
|
|
79
|
+
});
|
|
80
|
+
heartbeat = setInterval(() => {
|
|
81
|
+
enqueue(`: heartbeat\n\n`);
|
|
82
|
+
}, heartbeatMs);
|
|
83
|
+
lifetime = setTimeout(() => {
|
|
84
|
+
try {
|
|
85
|
+
controller.close();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Already closed; nothing to do.
|
|
89
|
+
}
|
|
90
|
+
}, maxLifetimeMs);
|
|
91
|
+
},
|
|
92
|
+
cancel() {
|
|
93
|
+
if (heartbeat)
|
|
94
|
+
clearInterval(heartbeat);
|
|
95
|
+
if (lifetime)
|
|
96
|
+
clearTimeout(lifetime);
|
|
97
|
+
if (unsubscribe)
|
|
98
|
+
unsubscribe();
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Standard SSE response headers. Exposed for callers (and tests) that want
|
|
104
|
+
* to wrap the stream themselves.
|
|
105
|
+
*/
|
|
106
|
+
export const PREVIEW_SSE_HEADERS = {
|
|
107
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
108
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
109
|
+
Connection: 'keep-alive',
|
|
110
|
+
// Discourage proxies/CDNs from buffering — without this nginx will hold
|
|
111
|
+
// events back until 4 KB of payload accumulates.
|
|
112
|
+
'X-Accel-Buffering': 'no',
|
|
113
|
+
};
|
|
114
|
+
export function createPreviewStreamResponse(session, opts) {
|
|
115
|
+
return new Response(createPreviewStream(session, opts), {
|
|
116
|
+
status: 200,
|
|
117
|
+
headers: PREVIEW_SSE_HEADERS,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=sse-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse-stream.js","sourceRoot":"","sources":["../../src/preview/sse-stream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document comments — anchored, threaded discussion attached to documents.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the *pure* comment logic: validation, normalization, and
|
|
5
|
+
* the storage-agnostic CRUD entry points. The HTTP layer in `api/handlers.ts`
|
|
6
|
+
* is a thin wrapper that handles auth + JSON envelope; everything else lives
|
|
7
|
+
* here so the unit tests don't need a Request loop.
|
|
8
|
+
*
|
|
9
|
+
* Storage shape — see `packages/cms-core/prisma/cms-schema.prisma`,
|
|
10
|
+
* model `DocumentComment`. We require only a minimal Prisma surface so the
|
|
11
|
+
* helpers can be tested against an in-memory fake.
|
|
12
|
+
*
|
|
13
|
+
* Anchors are Yjs *relative positions* serialised as Base64. Storing them in
|
|
14
|
+
* a JSON column (rather than Bytes) keeps the REST contract trivial: the
|
|
15
|
+
* client posts `{ from: "AQ...", to: "AQ..." }`, we round-trip the strings.
|
|
16
|
+
* Slice 5 reconstitutes them with `Y.createAbsolutePositionFromRelativePosition`
|
|
17
|
+
* to paint anchored highlights in the editor; nothing in the API layer needs
|
|
18
|
+
* to interpret the bytes.
|
|
19
|
+
*/
|
|
20
|
+
/** Maximum length we accept for a comment body (markdown source). */
|
|
21
|
+
export declare const MAX_COMMENT_BODY_LENGTH = 10000;
|
|
22
|
+
/** Cap on returned comments per `listComments` call. */
|
|
23
|
+
export declare const MAX_COMMENT_LIST_PAGE_SIZE = 200;
|
|
24
|
+
/** Default page size when the caller does not request one. */
|
|
25
|
+
export declare const DEFAULT_COMMENT_LIST_PAGE_SIZE = 100;
|
|
26
|
+
/**
|
|
27
|
+
* Yjs relative-position anchor. The two ends are Base64-encoded
|
|
28
|
+
* `Y.encodeRelativePosition` bytes; slice 5 turns them back into absolute
|
|
29
|
+
* positions for rendering.
|
|
30
|
+
*
|
|
31
|
+
* Slice 4 (this module) treats the anchor as opaque: we only validate that
|
|
32
|
+
* each side is a reasonably short string so we don't store arbitrary blobs.
|
|
33
|
+
*/
|
|
34
|
+
export interface CommentAnchor {
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Public REST shape of a comment. Mirrors the Prisma row plus a derived
|
|
40
|
+
* `isResolved` flag so the client doesn't have to repeat the null check.
|
|
41
|
+
*/
|
|
42
|
+
export interface CommentDTO {
|
|
43
|
+
id: string;
|
|
44
|
+
documentId: string;
|
|
45
|
+
parentId: string | null;
|
|
46
|
+
userId: string | null;
|
|
47
|
+
body: string;
|
|
48
|
+
anchor: CommentAnchor | null;
|
|
49
|
+
resolvedAt: string | null;
|
|
50
|
+
resolvedById: string | null;
|
|
51
|
+
deletedAt: string | null;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
updatedAt: string;
|
|
54
|
+
isResolved: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Input for creating a new comment. */
|
|
57
|
+
export interface CreateCommentInput {
|
|
58
|
+
documentId: string;
|
|
59
|
+
userId: string;
|
|
60
|
+
body: string;
|
|
61
|
+
anchor?: CommentAnchor | null;
|
|
62
|
+
parentId?: string | null;
|
|
63
|
+
}
|
|
64
|
+
/** Input for updating an existing comment. Only the body is editable. */
|
|
65
|
+
export interface UpdateCommentInput {
|
|
66
|
+
body: string;
|
|
67
|
+
}
|
|
68
|
+
/** Filter passed to `listComments`. */
|
|
69
|
+
export interface ListCommentsFilter {
|
|
70
|
+
documentId: string;
|
|
71
|
+
/** Include resolved threads. Default: `false`. */
|
|
72
|
+
includeResolved?: boolean;
|
|
73
|
+
/** Include soft-deleted comments. Default: `false`. */
|
|
74
|
+
includeDeleted?: boolean;
|
|
75
|
+
/** Page size (1..`MAX_COMMENT_LIST_PAGE_SIZE`). */
|
|
76
|
+
pageSize?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Minimal Prisma surface we depend on. The real `PrismaClient` satisfies it,
|
|
80
|
+
* but tests inject a small in-memory fake.
|
|
81
|
+
*/
|
|
82
|
+
export interface CommentsDB {
|
|
83
|
+
documentComment: {
|
|
84
|
+
findUnique: (args: {
|
|
85
|
+
where: {
|
|
86
|
+
id: string;
|
|
87
|
+
};
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
}) => Promise<DocumentCommentRow | null>;
|
|
90
|
+
findMany: (args: {
|
|
91
|
+
where: Record<string, unknown>;
|
|
92
|
+
orderBy?: Record<string, 'asc' | 'desc'>;
|
|
93
|
+
take?: number;
|
|
94
|
+
}) => Promise<DocumentCommentRow[]>;
|
|
95
|
+
create: (args: {
|
|
96
|
+
data: {
|
|
97
|
+
documentId: string;
|
|
98
|
+
userId: string | null;
|
|
99
|
+
body: string;
|
|
100
|
+
anchor: CommentAnchor | null;
|
|
101
|
+
parentId: string | null;
|
|
102
|
+
};
|
|
103
|
+
}) => Promise<DocumentCommentRow>;
|
|
104
|
+
update: (args: {
|
|
105
|
+
where: {
|
|
106
|
+
id: string;
|
|
107
|
+
};
|
|
108
|
+
data: Partial<{
|
|
109
|
+
body: string;
|
|
110
|
+
resolvedAt: Date | null;
|
|
111
|
+
resolvedById: string | null;
|
|
112
|
+
deletedAt: Date | null;
|
|
113
|
+
}>;
|
|
114
|
+
}) => Promise<DocumentCommentRow>;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Row shape returned by Prisma. We accept Date or ISO strings so fakes are cheap. */
|
|
118
|
+
export interface DocumentCommentRow {
|
|
119
|
+
id: string;
|
|
120
|
+
documentId: string;
|
|
121
|
+
parentId: string | null;
|
|
122
|
+
userId: string | null;
|
|
123
|
+
body: string;
|
|
124
|
+
anchor: CommentAnchor | null;
|
|
125
|
+
resolvedAt: Date | string | null;
|
|
126
|
+
resolvedById: string | null;
|
|
127
|
+
deletedAt: Date | string | null;
|
|
128
|
+
createdAt: Date | string;
|
|
129
|
+
updatedAt: Date | string;
|
|
130
|
+
}
|
|
131
|
+
/** Discriminated result type used by the service entry points. */
|
|
132
|
+
export type CommentsResult<T> = {
|
|
133
|
+
ok: true;
|
|
134
|
+
value: T;
|
|
135
|
+
} | {
|
|
136
|
+
ok: false;
|
|
137
|
+
error: CommentsError;
|
|
138
|
+
};
|
|
139
|
+
export interface CommentsError {
|
|
140
|
+
/**
|
|
141
|
+
* Stable machine-readable code. The HTTP layer maps each to a status:
|
|
142
|
+
* - validation → 400
|
|
143
|
+
* - not_found → 404
|
|
144
|
+
* - forbidden → 403
|
|
145
|
+
* - conflict → 409 (e.g. resolving an already-resolved thread)
|
|
146
|
+
*/
|
|
147
|
+
code: 'validation' | 'not_found' | 'forbidden' | 'conflict';
|
|
148
|
+
message: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Strip a comment row down to the public DTO. Centralised so the response
|
|
152
|
+
* shape stays consistent between create / list / update / resolve.
|
|
153
|
+
*/
|
|
154
|
+
export declare function toCommentDTO(row: DocumentCommentRow): CommentDTO;
|
|
155
|
+
/**
|
|
156
|
+
* Parse + validate an anchor blob coming from an untrusted client. Returns
|
|
157
|
+
* `null` when the caller did not provide one (document-level comments).
|
|
158
|
+
*/
|
|
159
|
+
export declare function parseAnchor(input: unknown): CommentsResult<CommentAnchor | null>;
|
|
160
|
+
/** Normalize whitespace + enforce length limits on a posted body. */
|
|
161
|
+
export declare function normaliseBody(input: unknown): CommentsResult<string>;
|
|
162
|
+
/**
|
|
163
|
+
* Create a comment. Enforces:
|
|
164
|
+
* - body validation (non-empty, length-capped)
|
|
165
|
+
* - anchor shape validation
|
|
166
|
+
* - single-level threading: a `parentId` must reference an existing,
|
|
167
|
+
* non-deleted, non-reply comment under the same document.
|
|
168
|
+
*
|
|
169
|
+
* The caller (HTTP handler) is responsible for auth — once we reach here we
|
|
170
|
+
* trust the supplied `userId`.
|
|
171
|
+
*/
|
|
172
|
+
export declare function createComment(db: CommentsDB, input: CreateCommentInput): Promise<CommentsResult<CommentDTO>>;
|
|
173
|
+
/**
|
|
174
|
+
* List comments for a document. Returns the rows ordered oldest-first so a
|
|
175
|
+
* naive client can render threads top-down. Pagination is "show the first
|
|
176
|
+
* N"; the higher-level UI in slice 5 will switch to cursor pagination once
|
|
177
|
+
* volume justifies it.
|
|
178
|
+
*/
|
|
179
|
+
export declare function listComments(db: CommentsDB, filter: ListCommentsFilter): Promise<CommentsResult<CommentDTO[]>>;
|
|
180
|
+
/**
|
|
181
|
+
* Edit a comment body. Only the author can edit, and only if the comment is
|
|
182
|
+
* not soft-deleted. Resolved threads remain editable so a moderator can
|
|
183
|
+
* append context.
|
|
184
|
+
*/
|
|
185
|
+
export declare function updateComment(db: CommentsDB, id: string, input: UpdateCommentInput, actor: {
|
|
186
|
+
userId: string;
|
|
187
|
+
isAdmin: boolean;
|
|
188
|
+
}): Promise<CommentsResult<CommentDTO>>;
|
|
189
|
+
/**
|
|
190
|
+
* Mark a comment thread as resolved. Either the comment's author or any
|
|
191
|
+
* admin / write-role caller can resolve. Resolving an already-resolved
|
|
192
|
+
* comment is a no-op (returns the existing row) so a duplicate click in the
|
|
193
|
+
* UI does not surface as an error.
|
|
194
|
+
*/
|
|
195
|
+
export declare function resolveComment(db: CommentsDB, id: string, actor: {
|
|
196
|
+
userId: string;
|
|
197
|
+
canResolve: boolean;
|
|
198
|
+
}): Promise<CommentsResult<CommentDTO>>;
|
|
199
|
+
/**
|
|
200
|
+
* Re-open a resolved thread. Same permission as `resolveComment`.
|
|
201
|
+
*/
|
|
202
|
+
export declare function reopenComment(db: CommentsDB, id: string, actor: {
|
|
203
|
+
userId: string;
|
|
204
|
+
canResolve: boolean;
|
|
205
|
+
}): Promise<CommentsResult<CommentDTO>>;
|
|
206
|
+
/**
|
|
207
|
+
* Soft-delete a comment. The row is preserved (so replies don't lose their
|
|
208
|
+
* thread root) but stops being returned by `listComments` unless explicitly
|
|
209
|
+
* requested with `includeDeleted: true`.
|
|
210
|
+
*
|
|
211
|
+
* Only the author or an admin may delete.
|
|
212
|
+
*/
|
|
213
|
+
export declare function deleteComment(db: CommentsDB, id: string, actor: {
|
|
214
|
+
userId: string;
|
|
215
|
+
isAdmin: boolean;
|
|
216
|
+
}): Promise<CommentsResult<CommentDTO>>;
|
|
217
|
+
//# sourceMappingURL=comments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/realtime/comments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,qEAAqE;AACrE,eAAO,MAAM,uBAAuB,QAAS,CAAA;AAE7C,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAE7C,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,MAAM,CAAA;AAEjD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAID;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;CACpB;AAED,wCAAwC;AACxC,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,yEAAyE;AACzE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;CACb;AAED,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,kDAAkD;IAClD,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,uDAAuD;IACvD,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE;QACf,UAAU,EAAE,CAAC,IAAI,EAAE;YACjB,KAAK,EAAE;gBAAE,EAAE,EAAE,MAAM,CAAA;aAAE,CAAA;YAErB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;SACvB,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAA;QACxC,QAAQ,EAAE,CAAC,IAAI,EAAE;YACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAC9B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,CAAA;YACxC,IAAI,CAAC,EAAE,MAAM,CAAA;SACd,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;QACnC,MAAM,EAAE,CAAC,IAAI,EAAE;YACb,IAAI,EAAE;gBACJ,UAAU,EAAE,MAAM,CAAA;gBAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;gBACrB,IAAI,EAAE,MAAM,CAAA;gBACZ,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;gBAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;aACxB,CAAA;SACF,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;QACjC,MAAM,EAAE,CAAC,IAAI,EAAE;YACb,KAAK,EAAE;gBAAE,EAAE,EAAE,MAAM,CAAA;aAAE,CAAA;YACrB,IAAI,EAAE,OAAO,CAAC;gBACZ,IAAI,EAAE,MAAM,CAAA;gBACZ,UAAU,EAAE,IAAI,GAAG,IAAI,CAAA;gBACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;gBAC3B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAA;aACvB,CAAC,CAAA;SACH,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;KAClC,CAAA;CACF;AAED,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAChC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAC/B,SAAS,EAAE,IAAI,GAAG,MAAM,CAAA;IACxB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAA;CACzB;AAED,kEAAkE;AAClE,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAA;AAE5F,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,IAAI,EAAE,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,CAAA;IAC3D,OAAO,EAAE,MAAM,CAAA;CAChB;AAiBD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,kBAAkB,GAAG,UAAU,CAehE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAAC,aAAa,GAAG,IAAI,CAAC,CAuBhF;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAepE;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgDrC;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,cAAc,CAAC,UAAU,EAAE,CAAC,CAAC,CAevC;AASD;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,kBAAkB,EACzB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgBrC;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAC7C,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgBrC;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,GAC7C,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgBrC;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC1C,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAgBrC"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document comments — anchored, threaded discussion attached to documents.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the *pure* comment logic: validation, normalization, and
|
|
5
|
+
* the storage-agnostic CRUD entry points. The HTTP layer in `api/handlers.ts`
|
|
6
|
+
* is a thin wrapper that handles auth + JSON envelope; everything else lives
|
|
7
|
+
* here so the unit tests don't need a Request loop.
|
|
8
|
+
*
|
|
9
|
+
* Storage shape — see `packages/cms-core/prisma/cms-schema.prisma`,
|
|
10
|
+
* model `DocumentComment`. We require only a minimal Prisma surface so the
|
|
11
|
+
* helpers can be tested against an in-memory fake.
|
|
12
|
+
*
|
|
13
|
+
* Anchors are Yjs *relative positions* serialised as Base64. Storing them in
|
|
14
|
+
* a JSON column (rather than Bytes) keeps the REST contract trivial: the
|
|
15
|
+
* client posts `{ from: "AQ...", to: "AQ..." }`, we round-trip the strings.
|
|
16
|
+
* Slice 5 reconstitutes them with `Y.createAbsolutePositionFromRelativePosition`
|
|
17
|
+
* to paint anchored highlights in the editor; nothing in the API layer needs
|
|
18
|
+
* to interpret the bytes.
|
|
19
|
+
*/
|
|
20
|
+
/** Maximum length we accept for a comment body (markdown source). */
|
|
21
|
+
export const MAX_COMMENT_BODY_LENGTH = 10_000;
|
|
22
|
+
/** Cap on returned comments per `listComments` call. */
|
|
23
|
+
export const MAX_COMMENT_LIST_PAGE_SIZE = 200;
|
|
24
|
+
/** Default page size when the caller does not request one. */
|
|
25
|
+
export const DEFAULT_COMMENT_LIST_PAGE_SIZE = 100;
|
|
26
|
+
const MAX_ANCHOR_FIELD_LENGTH = 4_096;
|
|
27
|
+
function err(code, message) {
|
|
28
|
+
return { code, message };
|
|
29
|
+
}
|
|
30
|
+
function toIso(value) {
|
|
31
|
+
if (value === null)
|
|
32
|
+
return null;
|
|
33
|
+
if (value instanceof Date)
|
|
34
|
+
return value.toISOString();
|
|
35
|
+
// Tolerate fakes that already store ISO strings.
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function toIsoRequired(value) {
|
|
39
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Strip a comment row down to the public DTO. Centralised so the response
|
|
43
|
+
* shape stays consistent between create / list / update / resolve.
|
|
44
|
+
*/
|
|
45
|
+
export function toCommentDTO(row) {
|
|
46
|
+
return {
|
|
47
|
+
id: row.id,
|
|
48
|
+
documentId: row.documentId,
|
|
49
|
+
parentId: row.parentId,
|
|
50
|
+
userId: row.userId,
|
|
51
|
+
body: row.body,
|
|
52
|
+
anchor: row.anchor,
|
|
53
|
+
resolvedAt: toIso(row.resolvedAt),
|
|
54
|
+
resolvedById: row.resolvedById,
|
|
55
|
+
deletedAt: toIso(row.deletedAt),
|
|
56
|
+
createdAt: toIsoRequired(row.createdAt),
|
|
57
|
+
updatedAt: toIsoRequired(row.updatedAt),
|
|
58
|
+
isResolved: row.resolvedAt !== null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse + validate an anchor blob coming from an untrusted client. Returns
|
|
63
|
+
* `null` when the caller did not provide one (document-level comments).
|
|
64
|
+
*/
|
|
65
|
+
export function parseAnchor(input) {
|
|
66
|
+
if (input === undefined || input === null) {
|
|
67
|
+
return { ok: true, value: null };
|
|
68
|
+
}
|
|
69
|
+
if (typeof input !== 'object' || Array.isArray(input)) {
|
|
70
|
+
return { ok: false, error: err('validation', 'anchor must be an object with `from` and `to`') };
|
|
71
|
+
}
|
|
72
|
+
const obj = input;
|
|
73
|
+
if (typeof obj.from !== 'string' || typeof obj.to !== 'string') {
|
|
74
|
+
return { ok: false, error: err('validation', 'anchor.from and anchor.to must be strings') };
|
|
75
|
+
}
|
|
76
|
+
if (obj.from.length === 0 ||
|
|
77
|
+
obj.from.length > MAX_ANCHOR_FIELD_LENGTH ||
|
|
78
|
+
obj.to.length === 0 ||
|
|
79
|
+
obj.to.length > MAX_ANCHOR_FIELD_LENGTH) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: err('validation', `anchor fields must be 1..${MAX_ANCHOR_FIELD_LENGTH} chars`),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { ok: true, value: { from: obj.from, to: obj.to } };
|
|
86
|
+
}
|
|
87
|
+
/** Normalize whitespace + enforce length limits on a posted body. */
|
|
88
|
+
export function normaliseBody(input) {
|
|
89
|
+
if (typeof input !== 'string') {
|
|
90
|
+
return { ok: false, error: err('validation', 'body must be a string') };
|
|
91
|
+
}
|
|
92
|
+
const trimmed = input.trim();
|
|
93
|
+
if (trimmed.length === 0) {
|
|
94
|
+
return { ok: false, error: err('validation', 'body cannot be empty') };
|
|
95
|
+
}
|
|
96
|
+
if (trimmed.length > MAX_COMMENT_BODY_LENGTH) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: err('validation', `body cannot exceed ${MAX_COMMENT_BODY_LENGTH} characters`),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, value: trimmed };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a comment. Enforces:
|
|
106
|
+
* - body validation (non-empty, length-capped)
|
|
107
|
+
* - anchor shape validation
|
|
108
|
+
* - single-level threading: a `parentId` must reference an existing,
|
|
109
|
+
* non-deleted, non-reply comment under the same document.
|
|
110
|
+
*
|
|
111
|
+
* The caller (HTTP handler) is responsible for auth — once we reach here we
|
|
112
|
+
* trust the supplied `userId`.
|
|
113
|
+
*/
|
|
114
|
+
export async function createComment(db, input) {
|
|
115
|
+
if (typeof input.documentId !== 'string' || input.documentId.length === 0) {
|
|
116
|
+
return { ok: false, error: err('validation', 'documentId is required') };
|
|
117
|
+
}
|
|
118
|
+
if (typeof input.userId !== 'string' || input.userId.length === 0) {
|
|
119
|
+
return { ok: false, error: err('validation', 'userId is required') };
|
|
120
|
+
}
|
|
121
|
+
const body = normaliseBody(input.body);
|
|
122
|
+
if (!body.ok)
|
|
123
|
+
return body;
|
|
124
|
+
const anchor = parseAnchor(input.anchor);
|
|
125
|
+
if (!anchor.ok)
|
|
126
|
+
return anchor;
|
|
127
|
+
let parentId = null;
|
|
128
|
+
if (input.parentId != null && input.parentId !== '') {
|
|
129
|
+
if (typeof input.parentId !== 'string') {
|
|
130
|
+
return { ok: false, error: err('validation', 'parentId must be a string') };
|
|
131
|
+
}
|
|
132
|
+
const parent = await db.documentComment.findUnique({ where: { id: input.parentId } });
|
|
133
|
+
if (!parent) {
|
|
134
|
+
return { ok: false, error: err('not_found', 'parent comment not found') };
|
|
135
|
+
}
|
|
136
|
+
if (parent.documentId !== input.documentId) {
|
|
137
|
+
return { ok: false, error: err('validation', 'parent comment is on a different document') };
|
|
138
|
+
}
|
|
139
|
+
if (parent.parentId !== null) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: err('validation', 'replies cannot themselves be replied to (one level only)'),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (parent.deletedAt !== null) {
|
|
146
|
+
return { ok: false, error: err('not_found', 'parent comment has been deleted') };
|
|
147
|
+
}
|
|
148
|
+
parentId = parent.id;
|
|
149
|
+
}
|
|
150
|
+
const row = await db.documentComment.create({
|
|
151
|
+
data: {
|
|
152
|
+
documentId: input.documentId,
|
|
153
|
+
userId: input.userId,
|
|
154
|
+
body: body.value,
|
|
155
|
+
anchor: anchor.value,
|
|
156
|
+
parentId,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return { ok: true, value: toCommentDTO(row) };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* List comments for a document. Returns the rows ordered oldest-first so a
|
|
163
|
+
* naive client can render threads top-down. Pagination is "show the first
|
|
164
|
+
* N"; the higher-level UI in slice 5 will switch to cursor pagination once
|
|
165
|
+
* volume justifies it.
|
|
166
|
+
*/
|
|
167
|
+
export async function listComments(db, filter) {
|
|
168
|
+
if (typeof filter.documentId !== 'string' || filter.documentId.length === 0) {
|
|
169
|
+
return { ok: false, error: err('validation', 'documentId is required') };
|
|
170
|
+
}
|
|
171
|
+
const where = { documentId: filter.documentId };
|
|
172
|
+
if (!filter.includeResolved)
|
|
173
|
+
where.resolvedAt = null;
|
|
174
|
+
if (!filter.includeDeleted)
|
|
175
|
+
where.deletedAt = null;
|
|
176
|
+
const pageSize = clampPageSize(filter.pageSize);
|
|
177
|
+
const rows = await db.documentComment.findMany({
|
|
178
|
+
where,
|
|
179
|
+
orderBy: { createdAt: 'asc' },
|
|
180
|
+
take: pageSize,
|
|
181
|
+
});
|
|
182
|
+
return { ok: true, value: rows.map(toCommentDTO) };
|
|
183
|
+
}
|
|
184
|
+
function clampPageSize(n) {
|
|
185
|
+
if (n === undefined || Number.isNaN(n))
|
|
186
|
+
return DEFAULT_COMMENT_LIST_PAGE_SIZE;
|
|
187
|
+
if (n < 1)
|
|
188
|
+
return 1;
|
|
189
|
+
if (n > MAX_COMMENT_LIST_PAGE_SIZE)
|
|
190
|
+
return MAX_COMMENT_LIST_PAGE_SIZE;
|
|
191
|
+
return Math.floor(n);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Edit a comment body. Only the author can edit, and only if the comment is
|
|
195
|
+
* not soft-deleted. Resolved threads remain editable so a moderator can
|
|
196
|
+
* append context.
|
|
197
|
+
*/
|
|
198
|
+
export async function updateComment(db, id, input, actor) {
|
|
199
|
+
const existing = await db.documentComment.findUnique({ where: { id } });
|
|
200
|
+
if (!existing || existing.deletedAt !== null) {
|
|
201
|
+
return { ok: false, error: err('not_found', 'comment not found') };
|
|
202
|
+
}
|
|
203
|
+
if (!actor.isAdmin && existing.userId !== actor.userId) {
|
|
204
|
+
return { ok: false, error: err('forbidden', 'only the author can edit this comment') };
|
|
205
|
+
}
|
|
206
|
+
const body = normaliseBody(input.body);
|
|
207
|
+
if (!body.ok)
|
|
208
|
+
return body;
|
|
209
|
+
const row = await db.documentComment.update({
|
|
210
|
+
where: { id },
|
|
211
|
+
data: { body: body.value },
|
|
212
|
+
});
|
|
213
|
+
return { ok: true, value: toCommentDTO(row) };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Mark a comment thread as resolved. Either the comment's author or any
|
|
217
|
+
* admin / write-role caller can resolve. Resolving an already-resolved
|
|
218
|
+
* comment is a no-op (returns the existing row) so a duplicate click in the
|
|
219
|
+
* UI does not surface as an error.
|
|
220
|
+
*/
|
|
221
|
+
export async function resolveComment(db, id, actor) {
|
|
222
|
+
const existing = await db.documentComment.findUnique({ where: { id } });
|
|
223
|
+
if (!existing || existing.deletedAt !== null) {
|
|
224
|
+
return { ok: false, error: err('not_found', 'comment not found') };
|
|
225
|
+
}
|
|
226
|
+
if (!actor.canResolve && existing.userId !== actor.userId) {
|
|
227
|
+
return { ok: false, error: err('forbidden', 'not allowed to resolve this comment') };
|
|
228
|
+
}
|
|
229
|
+
if (existing.resolvedAt !== null) {
|
|
230
|
+
return { ok: true, value: toCommentDTO(existing) };
|
|
231
|
+
}
|
|
232
|
+
const row = await db.documentComment.update({
|
|
233
|
+
where: { id },
|
|
234
|
+
data: { resolvedAt: new Date(), resolvedById: actor.userId },
|
|
235
|
+
});
|
|
236
|
+
return { ok: true, value: toCommentDTO(row) };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Re-open a resolved thread. Same permission as `resolveComment`.
|
|
240
|
+
*/
|
|
241
|
+
export async function reopenComment(db, id, actor) {
|
|
242
|
+
const existing = await db.documentComment.findUnique({ where: { id } });
|
|
243
|
+
if (!existing || existing.deletedAt !== null) {
|
|
244
|
+
return { ok: false, error: err('not_found', 'comment not found') };
|
|
245
|
+
}
|
|
246
|
+
if (!actor.canResolve && existing.userId !== actor.userId) {
|
|
247
|
+
return { ok: false, error: err('forbidden', 'not allowed to re-open this comment') };
|
|
248
|
+
}
|
|
249
|
+
if (existing.resolvedAt === null) {
|
|
250
|
+
return { ok: true, value: toCommentDTO(existing) };
|
|
251
|
+
}
|
|
252
|
+
const row = await db.documentComment.update({
|
|
253
|
+
where: { id },
|
|
254
|
+
data: { resolvedAt: null, resolvedById: null },
|
|
255
|
+
});
|
|
256
|
+
return { ok: true, value: toCommentDTO(row) };
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Soft-delete a comment. The row is preserved (so replies don't lose their
|
|
260
|
+
* thread root) but stops being returned by `listComments` unless explicitly
|
|
261
|
+
* requested with `includeDeleted: true`.
|
|
262
|
+
*
|
|
263
|
+
* Only the author or an admin may delete.
|
|
264
|
+
*/
|
|
265
|
+
export async function deleteComment(db, id, actor) {
|
|
266
|
+
const existing = await db.documentComment.findUnique({ where: { id } });
|
|
267
|
+
if (!existing) {
|
|
268
|
+
return { ok: false, error: err('not_found', 'comment not found') };
|
|
269
|
+
}
|
|
270
|
+
if (!actor.isAdmin && existing.userId !== actor.userId) {
|
|
271
|
+
return { ok: false, error: err('forbidden', 'only the author can delete this comment') };
|
|
272
|
+
}
|
|
273
|
+
if (existing.deletedAt !== null) {
|
|
274
|
+
return { ok: true, value: toCommentDTO(existing) };
|
|
275
|
+
}
|
|
276
|
+
const row = await db.documentComment.update({
|
|
277
|
+
where: { id },
|
|
278
|
+
data: { deletedAt: new Date() },
|
|
279
|
+
});
|
|
280
|
+
return { ok: true, value: toCommentDTO(row) };
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.js","sourceRoot":"","sources":["../../src/realtime/comments.ts"],"names":[],"mappings":"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"}
|