@sanity/sdk 2.19.0 → 2.20.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/_exports/_internal.d.ts +2 -2
- package/dist/_exports/_internal.js +196 -22
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/authStore-CWTAuieY.js +1072 -0
- package/dist/authStore-CWTAuieY.js.map +1 -0
- package/dist/clientStore-CMzJ7XhZ.js +168 -0
- package/dist/clientStore-CMzJ7XhZ.js.map +1 -0
- package/dist/createGroqSearchFilter-B0NdA-EP.js +1222 -0
- package/dist/createGroqSearchFilter-B0NdA-EP.js.map +1 -0
- package/dist/getEnv-76AjZd_f.js +178 -0
- package/dist/getEnv-76AjZd_f.js.map +1 -0
- package/dist/index.d.ts +2 -2105
- package/dist/index.js +4140 -3870
- package/dist/index.js.map +1 -1
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/telemetryManager-Bd3O8z2S.js +127 -0
- package/dist/telemetryManager-Bd3O8z2S.js.map +1 -0
- package/dist/utils-BMwA8Kg9.d.ts +3334 -0
- package/dist/utils-BMwA8Kg9.d.ts.map +1 -0
- package/dist/version-DL2l07HG.js +9 -0
- package/dist/version-DL2l07HG.js.map +1 -0
- package/package.json +12 -20
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +30 -0
- package/src/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/document/documentConstants.ts +8 -0
- package/src/document/documentStore.test.ts +98 -1
- package/src/document/documentStore.ts +36 -5
- package/src/document/patchOperations.test.ts +27 -0
- package/src/document/patchOperations.ts +10 -1
- package/src/document/permissions.test.ts +3 -1
- package/src/document/reducers.test.ts +133 -0
- package/src/document/reducers.ts +18 -3
- package/src/projection/projectionStore.test.ts +0 -8
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -923
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
- package/dist/_chunks-es/_internal.js +0 -134
- package/dist/_chunks-es/_internal.js.map +0 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +0 -1590
- package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
- package/dist/_chunks-es/telemetryManager.js +0 -93
- package/dist/_chunks-es/telemetryManager.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import {omitProperty} from '../utils/object'
|
|
2
|
+
import {type CommentPostPayload, type StoredComment} from './types'
|
|
3
|
+
|
|
4
|
+
interface CommentsEntry {
|
|
5
|
+
/** `undefined` until the first snapshot arrives, which is what suspends readers. */
|
|
6
|
+
comments?: Record<string, StoredComment>
|
|
7
|
+
error?: unknown
|
|
8
|
+
subscribers: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommentsStoreState {
|
|
12
|
+
entries: {[key: string]: CommentsEntry | undefined}
|
|
13
|
+
/** Comment creates that have not been confirmed by the server yet. */
|
|
14
|
+
pendingCreates: {[commentId: string]: true | undefined}
|
|
15
|
+
/**
|
|
16
|
+
* The most recent transaction this client started per comment, held only
|
|
17
|
+
* while the write is in flight.
|
|
18
|
+
*
|
|
19
|
+
* The listener echoes our own writes back. Without this, an echo from an
|
|
20
|
+
* earlier transaction could arrive after a later one and undo it on screen.
|
|
21
|
+
* Cleared as soon as the write settles: applying our own echo after that is
|
|
22
|
+
* harmless, because it carries the same data we already show.
|
|
23
|
+
*/
|
|
24
|
+
pendingTransactions: {[commentId: string]: string | undefined}
|
|
25
|
+
error?: unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CommentsKeyParts {
|
|
29
|
+
documentId: string
|
|
30
|
+
documentVersionId?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getCommentsKey({documentId, documentVersionId}: CommentsKeyParts): string {
|
|
34
|
+
return JSON.stringify([documentId, documentVersionId ?? null])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseCommentsKey(key: string): CommentsKeyParts {
|
|
38
|
+
const [documentId, documentVersionId] = JSON.parse(key) as [string, string | null]
|
|
39
|
+
return {documentId, ...(documentVersionId ? {documentVersionId} : {})}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const addSubscriber =
|
|
43
|
+
(key: string, subscriptionId: string) =>
|
|
44
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
45
|
+
const entry = prev.entries[key]
|
|
46
|
+
const subscribers = [...(entry?.subscribers ?? []), subscriptionId]
|
|
47
|
+
return {...prev, entries: {...prev.entries, [key]: {...entry, subscribers}}}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const removeSubscriber =
|
|
51
|
+
(key: string, subscriptionId: string) =>
|
|
52
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
53
|
+
const entry = prev.entries[key]
|
|
54
|
+
if (!entry) return prev
|
|
55
|
+
const subscribers = entry.subscribers.filter((id) => id !== subscriptionId)
|
|
56
|
+
if (!subscribers.length) {
|
|
57
|
+
const pendingCreates = {...prev.pendingCreates}
|
|
58
|
+
const pendingTransactions = {...prev.pendingTransactions}
|
|
59
|
+
|
|
60
|
+
for (const commentId of Object.keys(entry.comments ?? {})) {
|
|
61
|
+
delete pendingCreates[commentId]
|
|
62
|
+
delete pendingTransactions[commentId]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...prev,
|
|
67
|
+
entries: omitProperty(prev.entries, key),
|
|
68
|
+
pendingCreates,
|
|
69
|
+
pendingTransactions,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {...prev, entries: {...prev.entries, [key]: {...entry, subscribers}}}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Replaces an entry's contents with a freshly fetched snapshot. */
|
|
76
|
+
export const setComments =
|
|
77
|
+
(key: string, comments: StoredComment[]) =>
|
|
78
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
79
|
+
const entry = prev.entries[key]
|
|
80
|
+
if (!entry) return prev
|
|
81
|
+
const byId = Object.fromEntries(comments.map((comment) => [comment._id, comment]))
|
|
82
|
+
const pendingCreates = {...prev.pendingCreates}
|
|
83
|
+
|
|
84
|
+
for (const [commentId, localComment] of Object.entries(entry.comments ?? {})) {
|
|
85
|
+
if (Object.hasOwn(byId, commentId)) {
|
|
86
|
+
delete pendingCreates[commentId]
|
|
87
|
+
} else if (Object.hasOwn(pendingCreates, commentId) || localComment._state) {
|
|
88
|
+
// A snapshot can race an in-flight create. Failed creates are also local
|
|
89
|
+
// drafts and must remain available for retry.
|
|
90
|
+
byId[commentId] = localComment
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...prev,
|
|
96
|
+
entries: {...prev.entries, [key]: {...entry, comments: byId, error: undefined}},
|
|
97
|
+
pendingCreates,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const setCommentsError =
|
|
102
|
+
(key: string, error: unknown) =>
|
|
103
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
104
|
+
const entry = prev.entries[key]
|
|
105
|
+
if (!entry) return prev
|
|
106
|
+
return {...prev, entries: {...prev.entries, [key]: {...entry, error}}}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A comment as it arrived from the server, replacing whatever we held. */
|
|
110
|
+
export const receiveComment =
|
|
111
|
+
(key: string, comment: StoredComment) =>
|
|
112
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
113
|
+
const entry = prev.entries[key]
|
|
114
|
+
const pendingCreates = omitProperty(prev.pendingCreates, comment._id)
|
|
115
|
+
if (!entry) return {...prev, pendingCreates}
|
|
116
|
+
return {
|
|
117
|
+
...prev,
|
|
118
|
+
pendingCreates,
|
|
119
|
+
entries: {
|
|
120
|
+
...prev.entries,
|
|
121
|
+
[key]: {...entry, comments: {...entry.comments, [comment._id]: comment}},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A comment we just wrote, shown before the server confirms it.
|
|
128
|
+
*
|
|
129
|
+
* `_createdAt` is assigned by the server, but the list sorts on it, so an
|
|
130
|
+
* optimistic comment needs a stand-in until the real value arrives.
|
|
131
|
+
*/
|
|
132
|
+
type OptimisticComment = CommentPostPayload & {_createdAt?: string; _rev?: string}
|
|
133
|
+
|
|
134
|
+
/** A create that already failed once is being retried, not written fresh. */
|
|
135
|
+
function isRetryingCreate(existing: StoredComment | undefined): boolean {
|
|
136
|
+
const state = existing?._state?.type
|
|
137
|
+
return state === 'createError' || state === 'createRetrying'
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mergeOptimisticComment(
|
|
141
|
+
existing: StoredComment | undefined,
|
|
142
|
+
comment: OptimisticComment,
|
|
143
|
+
): StoredComment {
|
|
144
|
+
return {
|
|
145
|
+
...existing,
|
|
146
|
+
...comment,
|
|
147
|
+
_createdAt: comment._createdAt ?? existing?._createdAt ?? new Date().toISOString(),
|
|
148
|
+
_rev: comment._rev ?? existing?._rev ?? '',
|
|
149
|
+
...(isRetryingCreate(existing) ? {_state: {type: 'createRetrying'} as const} : {}),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const addComment =
|
|
154
|
+
(key: string, comment: OptimisticComment) =>
|
|
155
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
156
|
+
const entry = prev.entries[key]
|
|
157
|
+
if (!entry) return prev
|
|
158
|
+
|
|
159
|
+
const existing =
|
|
160
|
+
entry.comments && Object.hasOwn(entry.comments, comment._id)
|
|
161
|
+
? entry.comments[comment._id]
|
|
162
|
+
: undefined
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...prev,
|
|
166
|
+
pendingCreates: {...prev.pendingCreates, [comment._id]: true},
|
|
167
|
+
entries: {
|
|
168
|
+
...prev.entries,
|
|
169
|
+
[key]: {
|
|
170
|
+
...entry,
|
|
171
|
+
comments: {
|
|
172
|
+
...entry.comments,
|
|
173
|
+
[comment._id]: mergeOptimisticComment(existing, comment),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Merges a partial update into whichever entry holds the comment.
|
|
182
|
+
*
|
|
183
|
+
* Comment ids are unique, so this touches one entry in practice. Searching by
|
|
184
|
+
* id rather than taking a key means callers editing a comment do not have to
|
|
185
|
+
* say which document it belongs to.
|
|
186
|
+
*/
|
|
187
|
+
export const applyCommentUpdate =
|
|
188
|
+
(commentId: string, patch: Partial<StoredComment>) =>
|
|
189
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
190
|
+
const entries = {...prev.entries}
|
|
191
|
+
let changed = false
|
|
192
|
+
|
|
193
|
+
for (const [key, entry] of Object.entries(prev.entries)) {
|
|
194
|
+
const comment = entry?.comments?.[commentId]
|
|
195
|
+
if (!entry || !comment) continue
|
|
196
|
+
changed = true
|
|
197
|
+
entries[key] = {
|
|
198
|
+
...entry,
|
|
199
|
+
comments: {...entry.comments, [commentId]: {...comment, ...patch}},
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return changed ? {...prev, entries} : prev
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Removes a comment and, when it is a thread parent, its replies. */
|
|
207
|
+
export const removeCommentById =
|
|
208
|
+
(commentId: string) =>
|
|
209
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
210
|
+
const entries = {...prev.entries}
|
|
211
|
+
const removedIds = new Set<string>()
|
|
212
|
+
let changed = false
|
|
213
|
+
|
|
214
|
+
for (const [key, entry] of Object.entries(prev.entries)) {
|
|
215
|
+
if (!entry?.comments) continue
|
|
216
|
+
|
|
217
|
+
const remaining = Object.fromEntries(
|
|
218
|
+
Object.entries(entry.comments).filter(([id, comment]) => {
|
|
219
|
+
const keep = id !== commentId && comment.parentCommentId !== commentId
|
|
220
|
+
if (!keep) removedIds.add(id)
|
|
221
|
+
return keep
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if (Object.keys(remaining).length === Object.keys(entry.comments).length) continue
|
|
226
|
+
|
|
227
|
+
changed = true
|
|
228
|
+
entries[key] = {...entry, comments: remaining}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!changed) return prev
|
|
232
|
+
|
|
233
|
+
const pendingCreates = {...prev.pendingCreates}
|
|
234
|
+
const pendingTransactions = {...prev.pendingTransactions}
|
|
235
|
+
for (const id of removedIds) {
|
|
236
|
+
delete pendingCreates[id]
|
|
237
|
+
delete pendingTransactions[id]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {...prev, entries, pendingCreates, pendingTransactions}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const setPendingTransaction =
|
|
244
|
+
(commentId: string, transactionId: string) =>
|
|
245
|
+
(prev: CommentsStoreState): CommentsStoreState => ({
|
|
246
|
+
...prev,
|
|
247
|
+
pendingTransactions: {...prev.pendingTransactions, [commentId]: transactionId},
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
export const clearPendingTransaction =
|
|
251
|
+
(commentId: string) =>
|
|
252
|
+
(prev: CommentsStoreState): CommentsStoreState => ({
|
|
253
|
+
...prev,
|
|
254
|
+
pendingTransactions: omitProperty(prev.pendingTransactions, commentId),
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
/** Marks a pending create as failed, unless another request already confirmed it. */
|
|
258
|
+
export const setCommentCreateError =
|
|
259
|
+
(commentId: string, error: Error) =>
|
|
260
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
261
|
+
if (!Object.hasOwn(prev.pendingCreates, commentId)) return prev
|
|
262
|
+
const next = applyCommentUpdate(commentId, {_state: {type: 'createError', error}})(prev)
|
|
263
|
+
return {...next, pendingCreates: omitProperty(next.pendingCreates, commentId)}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Restores an optimistic edit if the failed transaction is still current. */
|
|
267
|
+
export const rollbackCommentUpdate =
|
|
268
|
+
(commentId: string, transactionId: string, previous: StoredComment) =>
|
|
269
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
270
|
+
if (
|
|
271
|
+
!Object.hasOwn(prev.pendingTransactions, commentId) ||
|
|
272
|
+
prev.pendingTransactions[commentId] !== transactionId
|
|
273
|
+
) {
|
|
274
|
+
return prev
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const entries = Object.fromEntries(
|
|
278
|
+
Object.entries(prev.entries).map(([key, entry]) => {
|
|
279
|
+
if (!entry?.comments?.[commentId]) return [key, entry]
|
|
280
|
+
return [key, {...entry, comments: {...entry.comments, [commentId]: previous}}]
|
|
281
|
+
}),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
...prev,
|
|
286
|
+
entries,
|
|
287
|
+
pendingTransactions: omitProperty(prev.pendingTransactions, commentId),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Restores comments removed optimistically when the server delete fails. */
|
|
292
|
+
export const restoreComments =
|
|
293
|
+
(removed: Array<{key: string; comments: StoredComment[]}>) =>
|
|
294
|
+
(prev: CommentsStoreState): CommentsStoreState => {
|
|
295
|
+
const entries = {...prev.entries}
|
|
296
|
+
let changed = false
|
|
297
|
+
|
|
298
|
+
for (const {key, comments} of removed) {
|
|
299
|
+
const entry = entries[key]
|
|
300
|
+
if (!entry) continue
|
|
301
|
+
const missing = comments.filter(
|
|
302
|
+
(comment) => !Object.hasOwn(entry.comments ?? {}, comment._id),
|
|
303
|
+
)
|
|
304
|
+
if (!missing.length) continue
|
|
305
|
+
changed = true
|
|
306
|
+
entries[key] = {
|
|
307
|
+
...entry,
|
|
308
|
+
comments: Object.fromEntries([
|
|
309
|
+
...Object.entries(entry.comments ?? {}),
|
|
310
|
+
...missing.map((comment) => [comment._id, comment] as const),
|
|
311
|
+
]),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return changed ? {...prev, entries} : prev
|
|
316
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {type PortableTextBlock} from '@sanity/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a thread is still being discussed or has been closed out.
|
|
5
|
+
* @beta
|
|
6
|
+
*/
|
|
7
|
+
export type CommentStatus = 'open' | 'resolved'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The body of a comment, as Portable Text.
|
|
11
|
+
*
|
|
12
|
+
* Mentions appear here as inline objects of type `mention` carrying a `userId`.
|
|
13
|
+
* The SDK stores and returns them untouched, so a mention written in the Studio
|
|
14
|
+
* survives a round trip, but there is no API for composing one yet.
|
|
15
|
+
* @beta
|
|
16
|
+
*/
|
|
17
|
+
export type CommentMessage = PortableTextBlock[] | null
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One block touched by a text selection, and that block's text with the
|
|
21
|
+
* selection boundaries marked.
|
|
22
|
+
*
|
|
23
|
+
* `text` is the block's *entire* plain text with two private-use sentinel
|
|
24
|
+
* characters (U+F000 and U+F001) inserted where the selection starts and ends.
|
|
25
|
+
* Storing marked-up text rather than character offsets is what lets a highlight
|
|
26
|
+
* survive edits elsewhere in the same block.
|
|
27
|
+
* @beta
|
|
28
|
+
*/
|
|
29
|
+
export interface CommentTextSelectionItem {
|
|
30
|
+
_key: string
|
|
31
|
+
text: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A comment anchored to a run of text inside a Portable Text field.
|
|
36
|
+
*
|
|
37
|
+
* The SDK passes this through untouched. Resolving it back to a position in a
|
|
38
|
+
* live editor needs the editor's current value, so that lives in
|
|
39
|
+
* `@portabletext/plugin-sdk-value` rather than here.
|
|
40
|
+
* @beta
|
|
41
|
+
*/
|
|
42
|
+
export interface CommentTextSelection {
|
|
43
|
+
type: 'text'
|
|
44
|
+
value: CommentTextSelectionItem[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An emoji reaction on a comment.
|
|
49
|
+
*
|
|
50
|
+
* Read only for now: a reaction added in the Studio shows up here, but there is
|
|
51
|
+
* no action for adding or removing one.
|
|
52
|
+
* @beta
|
|
53
|
+
*/
|
|
54
|
+
export interface CommentReaction {
|
|
55
|
+
shortName: string
|
|
56
|
+
userId: string
|
|
57
|
+
addedAt: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Why a comment is not yet on the server.
|
|
62
|
+
*
|
|
63
|
+
* Local only, never written. Present on a comment whose create request failed,
|
|
64
|
+
* so an app can offer a retry.
|
|
65
|
+
* @beta
|
|
66
|
+
*/
|
|
67
|
+
export type CommentLocalState = {type: 'createError'; error: Error} | {type: 'createRetrying'}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A single comment.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately not the stored document. Comments are moving from per-dataset
|
|
73
|
+
* addon datasets to an organization-level store, and the two disagree on the
|
|
74
|
+
* document type, on how the commented document is referenced, and on where the
|
|
75
|
+
* author is recorded. Everything here reads the same on both sides, so an app
|
|
76
|
+
* written against this survives the move.
|
|
77
|
+
*
|
|
78
|
+
* Threads are flat: every comment in a thread shares a `threadId`, the thread's
|
|
79
|
+
* first comment has no `parentCommentId`, and replies carry that first comment's
|
|
80
|
+
* `id`. Use {@link CommentThread} to work with them grouped.
|
|
81
|
+
*
|
|
82
|
+
* @beta
|
|
83
|
+
*/
|
|
84
|
+
export interface Comment {
|
|
85
|
+
id: string
|
|
86
|
+
createdAt: string
|
|
87
|
+
/**
|
|
88
|
+
* Who wrote it.
|
|
89
|
+
*
|
|
90
|
+
* Absent when the server carries the author instead of the document. The
|
|
91
|
+
* Studio's comments dataset stores it on the document, so it is present on
|
|
92
|
+
* everything written today, but an agent-authored comment records
|
|
93
|
+
* attribution elsewhere. Render a fallback rather than assuming a user id.
|
|
94
|
+
*/
|
|
95
|
+
authorId?: string
|
|
96
|
+
message: CommentMessage
|
|
97
|
+
threadId: string
|
|
98
|
+
/** Absent on the comment that starts a thread. */
|
|
99
|
+
parentCommentId?: string
|
|
100
|
+
status: CommentStatus
|
|
101
|
+
/** Set once the message has been rewritten. */
|
|
102
|
+
lastEditedAt?: string
|
|
103
|
+
/** The document the thread hangs on, always the published id. */
|
|
104
|
+
documentId: string
|
|
105
|
+
documentType: string
|
|
106
|
+
/** The field the thread hangs off, for example `title`. */
|
|
107
|
+
fieldPath: string
|
|
108
|
+
/** Set when the comment is anchored to a run of text in a Portable Text field. */
|
|
109
|
+
selection?: CommentTextSelection
|
|
110
|
+
/** A copy of the content the comment was written about. */
|
|
111
|
+
contentSnapshot?: unknown
|
|
112
|
+
reactions: CommentReaction[]
|
|
113
|
+
/** Local only. Present while a create is failing or being retried. */
|
|
114
|
+
state?: CommentLocalState
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A thread: one comment plus its replies.
|
|
119
|
+
*
|
|
120
|
+
* Unlike the Studio, the SDK returns every thread it finds. The Studio hides
|
|
121
|
+
* threads whose field is gone from the schema or hidden by a conditional, which
|
|
122
|
+
* it can do because it has the schema. Check `fieldPath` yourself if your app
|
|
123
|
+
* needs the same behaviour.
|
|
124
|
+
* @beta
|
|
125
|
+
*/
|
|
126
|
+
export interface CommentThread {
|
|
127
|
+
threadId: string
|
|
128
|
+
/** The field the thread hangs off, taken from its first comment. */
|
|
129
|
+
fieldPath: string
|
|
130
|
+
parentComment: Comment
|
|
131
|
+
/** Oldest first. */
|
|
132
|
+
replies: Comment[]
|
|
133
|
+
/** The parent plus its replies. */
|
|
134
|
+
commentsCount: number
|
|
135
|
+
/** Taken from the parent comment; replies follow it. */
|
|
136
|
+
status: CommentStatus
|
|
137
|
+
/** `createdAt` of the most recent comment in the thread. */
|
|
138
|
+
lastActivityAt: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Where in a document a thread hangs, as stored.
|
|
143
|
+
* @internal
|
|
144
|
+
*/
|
|
145
|
+
interface StoredCommentPath {
|
|
146
|
+
field: string
|
|
147
|
+
selection?: CommentTextSelection
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Ambient information the Studio records about where a comment was written.
|
|
152
|
+
*
|
|
153
|
+
* Written for the notification backend and read by nothing in the Studio UI.
|
|
154
|
+
* Not surfaced on {@link Comment}: the organization-level store types this as a
|
|
155
|
+
* free-form object, so nothing about the shape is worth promising.
|
|
156
|
+
*
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
interface StoredCommentContext {
|
|
160
|
+
tool: string
|
|
161
|
+
payload?: Record<string, unknown>
|
|
162
|
+
notification?: {
|
|
163
|
+
documentTitle: string
|
|
164
|
+
url?: string
|
|
165
|
+
workspaceTitle: string
|
|
166
|
+
workspaceName: string
|
|
167
|
+
currentThreadLength?: number
|
|
168
|
+
subscribers?: string[]
|
|
169
|
+
}
|
|
170
|
+
intent?: {
|
|
171
|
+
title: string
|
|
172
|
+
name: string
|
|
173
|
+
params: Record<string, unknown>
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A reaction as stored, including the array key.
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
181
|
+
interface StoredCommentReaction extends CommentReaction {
|
|
182
|
+
_key: string
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What a stored comment points at.
|
|
187
|
+
*
|
|
188
|
+
* Comments live in a separate addon dataset, so `document` is a
|
|
189
|
+
* `crossDatasetReference` back into the dataset holding the commented document.
|
|
190
|
+
* It is deliberately weak: a strong reference would stop Content Lake deleting
|
|
191
|
+
* the document it points at.
|
|
192
|
+
*
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
interface StoredCommentTarget {
|
|
196
|
+
path?: StoredCommentPath
|
|
197
|
+
documentRevisionId?: string
|
|
198
|
+
documentVersionId?: string
|
|
199
|
+
documentType: string
|
|
200
|
+
document:
|
|
201
|
+
| {
|
|
202
|
+
_dataset: string
|
|
203
|
+
_projectId: string
|
|
204
|
+
_ref: string
|
|
205
|
+
_type: 'crossDatasetReference'
|
|
206
|
+
_weak: boolean
|
|
207
|
+
}
|
|
208
|
+
| {
|
|
209
|
+
_ref: string
|
|
210
|
+
_type: 'reference'
|
|
211
|
+
_weak: boolean
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A comment exactly as the Studio stores it.
|
|
217
|
+
*
|
|
218
|
+
* Internal on purpose. This is the addon dataset's shape, and it changes when
|
|
219
|
+
* comments move to the organization-level store. {@link Comment} is what
|
|
220
|
+
* consumers get.
|
|
221
|
+
*
|
|
222
|
+
* @internal
|
|
223
|
+
*/
|
|
224
|
+
export interface StoredComment {
|
|
225
|
+
_type: 'comment'
|
|
226
|
+
_id: string
|
|
227
|
+
_createdAt: string
|
|
228
|
+
_rev: string
|
|
229
|
+
|
|
230
|
+
/** Local only. Never written to the server. */
|
|
231
|
+
_state?: CommentLocalState
|
|
232
|
+
|
|
233
|
+
authorId: string
|
|
234
|
+
message: CommentMessage
|
|
235
|
+
threadId: string
|
|
236
|
+
parentCommentId?: string
|
|
237
|
+
status: CommentStatus
|
|
238
|
+
lastEditedAt?: string
|
|
239
|
+
reactions: StoredCommentReaction[] | null
|
|
240
|
+
context?: StoredCommentContext
|
|
241
|
+
contentSnapshot?: unknown
|
|
242
|
+
target: StoredCommentTarget
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* What gets written when a comment is created.
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
249
|
+
export type CommentPostPayload = Omit<StoredComment, '_rev' | '_createdAt' | '_state'>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {weakenReferencesInContentSnapshot} from './weakenReferences'
|
|
4
|
+
|
|
5
|
+
describe('weakenReferencesInContentSnapshot', () => {
|
|
6
|
+
it('weakens a reference at the top level', () => {
|
|
7
|
+
expect(weakenReferencesInContentSnapshot({_ref: 'abc', _type: 'reference'})).toEqual({
|
|
8
|
+
_ref: 'abc',
|
|
9
|
+
_type: 'reference',
|
|
10
|
+
_weak: true,
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('weakens references nested in objects and arrays', () => {
|
|
15
|
+
expect(
|
|
16
|
+
weakenReferencesInContentSnapshot({
|
|
17
|
+
title: 'foo',
|
|
18
|
+
author: {_ref: 'bar', _type: 'reference'},
|
|
19
|
+
blocks: [{_key: 'a', tag: {_ref: 'baz', _type: 'reference'}}],
|
|
20
|
+
}),
|
|
21
|
+
).toEqual({
|
|
22
|
+
title: 'foo',
|
|
23
|
+
author: {_ref: 'bar', _type: 'reference', _weak: true},
|
|
24
|
+
blocks: [{_key: 'a', tag: {_ref: 'baz', _type: 'reference', _weak: true}}],
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('leaves an already weak reference weak', () => {
|
|
29
|
+
expect(weakenReferencesInContentSnapshot({_ref: 'abc', _weak: true})).toEqual({
|
|
30
|
+
_ref: 'abc',
|
|
31
|
+
_weak: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('passes primitives and null through', () => {
|
|
36
|
+
expect(weakenReferencesInContentSnapshot('foo')).toBe('foo')
|
|
37
|
+
expect(weakenReferencesInContentSnapshot(42)).toBe(42)
|
|
38
|
+
expect(weakenReferencesInContentSnapshot(null)).toBe(null)
|
|
39
|
+
expect(weakenReferencesInContentSnapshot(undefined)).toBe(undefined)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('does not mutate its input', () => {
|
|
43
|
+
const snapshot = {author: {_ref: 'bar'}}
|
|
44
|
+
weakenReferencesInContentSnapshot(snapshot)
|
|
45
|
+
expect(snapshot).toEqual({author: {_ref: 'bar'}})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function weaken(node: unknown): unknown {
|
|
2
|
+
if (Array.isArray(node)) {
|
|
3
|
+
return node.map(weaken)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (node && typeof node === 'object') {
|
|
7
|
+
if ('_ref' in node) {
|
|
8
|
+
return {...node, _weak: true}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, weaken(value)]))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return node
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Marks every reference in a content snapshot weak.
|
|
19
|
+
*
|
|
20
|
+
* A comment's snapshot is written into the addon dataset, but its references
|
|
21
|
+
* point at documents in the main one. Left strong, Content Lake would refuse to
|
|
22
|
+
* delete any document a snapshot happens to mention, so an old comment could
|
|
23
|
+
* pin a document forever.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function weakenReferencesInContentSnapshot(snapshot: unknown): unknown {
|
|
28
|
+
return weaken(snapshot)
|
|
29
|
+
}
|
|
@@ -15,3 +15,11 @@ export const API_VERSION = 'v2025-05-06'
|
|
|
15
15
|
*/
|
|
16
16
|
export const OUT_OF_SYNC_RETRY_BASE_DELAY = 500
|
|
17
17
|
export const OUT_OF_SYNC_RETRY_MAX_DELAY = 10_000
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base delay (ms) before retrying the dataset ACL fetch after a transient
|
|
21
|
+
* failure (a network error or 5xx response). Backoff doubles on each
|
|
22
|
+
* successive retry, capped at {@link ACL_RETRY_MAX_DELAY}.
|
|
23
|
+
*/
|
|
24
|
+
export const ACL_RETRY_BASE_DELAY = 500
|
|
25
|
+
export const ACL_RETRY_MAX_DELAY = 10_000
|