@sanity/sdk 3.0.0-rc.1 → 3.0.0-rc.2

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.
Files changed (120) hide show
  1. package/README.md +12 -0
  2. package/dist/_exports/_internal.d.ts +71 -3
  3. package/dist/_exports/_internal.d.ts.map +1 -1
  4. package/dist/_exports/_internal.js +332 -24
  5. package/dist/_exports/_internal.js.map +1 -1
  6. package/dist/_exports/agent.d.ts +65 -0
  7. package/dist/_exports/agent.d.ts.map +1 -0
  8. package/dist/_exports/agent.js +71 -0
  9. package/dist/_exports/agent.js.map +1 -0
  10. package/dist/_exports/comlink.d.ts +118 -0
  11. package/dist/_exports/comlink.d.ts.map +1 -0
  12. package/dist/_exports/comlink.js +69 -0
  13. package/dist/_exports/comlink.js.map +1 -0
  14. package/dist/authStore-CtE9bLHW.js +754 -0
  15. package/dist/authStore-CtE9bLHW.js.map +1 -0
  16. package/dist/authStore-DKFX8fn7.d.ts +3179 -0
  17. package/dist/authStore-DKFX8fn7.d.ts.map +1 -0
  18. package/dist/clientStore-BtE2-q2i.js +90 -0
  19. package/dist/clientStore-BtE2-q2i.js.map +1 -0
  20. package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
  21. package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
  22. package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
  23. package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
  24. package/dist/getEnv-D5RGkQLu.js +8 -0
  25. package/dist/getEnv-D5RGkQLu.js.map +1 -0
  26. package/dist/getNodeState-CR3__B2D.js +67 -0
  27. package/dist/getNodeState-CR3__B2D.js.map +1 -0
  28. package/dist/index.d.ts +3 -2335
  29. package/dist/index.js +4048 -3874
  30. package/dist/index.js.map +1 -1
  31. package/dist/logger-DAtJb5RQ.js +173 -0
  32. package/dist/logger-DAtJb5RQ.js.map +1 -0
  33. package/dist/object-CxXlSVg7.js +83 -0
  34. package/dist/object-CxXlSVg7.js.map +1 -0
  35. package/dist/queryStore-DPf9sN3V.js +1346 -0
  36. package/dist/queryStore-DPf9sN3V.js.map +1 -0
  37. package/dist/rolldown-runtime-vyAXikos.js +9 -0
  38. package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
  39. package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
  40. package/dist/telemetryManager-Dq_qCkeI.js +127 -0
  41. package/dist/telemetryManager-Dq_qCkeI.js.map +1 -0
  42. package/dist/version-BChjsi4u.js +9 -0
  43. package/dist/version-BChjsi4u.js.map +1 -0
  44. package/package.json +13 -19
  45. package/src/_exports/_internal.ts +1 -0
  46. package/src/_exports/agent.ts +19 -0
  47. package/src/_exports/comlink.ts +16 -0
  48. package/src/_exports/index.ts +38 -86
  49. package/src/applications/applications.test-d.ts +5 -0
  50. package/src/applications/applications.ts +5 -2
  51. package/src/auth/authMode.test.ts +0 -25
  52. package/src/auth/authMode.ts +3 -6
  53. package/src/auth/authStore.test.ts +1 -1
  54. package/src/auth/authStore.ts +0 -1
  55. package/src/comments/addonDatasetStore.test.ts +215 -0
  56. package/src/comments/addonDatasetStore.ts +249 -0
  57. package/src/comments/buildCommentThreads.test.ts +83 -0
  58. package/src/comments/buildCommentThreads.ts +54 -0
  59. package/src/comments/commentActions.test.ts +540 -0
  60. package/src/comments/commentActions.ts +555 -0
  61. package/src/comments/commentFieldPath.test.ts +40 -0
  62. package/src/comments/commentFieldPath.ts +44 -0
  63. package/src/comments/commentsConstants.ts +80 -0
  64. package/src/comments/commentsStore.test.ts +419 -0
  65. package/src/comments/commentsStore.ts +483 -0
  66. package/src/comments/normalizeComment.test.ts +116 -0
  67. package/src/comments/normalizeComment.ts +45 -0
  68. package/src/comments/observeComments.test.ts +193 -0
  69. package/src/comments/observeComments.ts +113 -0
  70. package/src/comments/reducers.test.ts +277 -0
  71. package/src/comments/reducers.ts +316 -0
  72. package/src/comments/types.ts +249 -0
  73. package/src/comments/weakenReferences.test.ts +47 -0
  74. package/src/comments/weakenReferences.ts +29 -0
  75. package/src/config/sanityConfig.ts +3 -75
  76. package/src/document/applyDocumentActions.test.ts +8 -12
  77. package/src/document/documentConstants.ts +8 -0
  78. package/src/document/documentStore.test.ts +104 -19
  79. package/src/document/documentStore.ts +36 -5
  80. package/src/document/patchOperations.test.ts +27 -0
  81. package/src/document/patchOperations.ts +10 -1
  82. package/src/document/permissions.test.ts +3 -1
  83. package/src/document/reducers.test.ts +133 -0
  84. package/src/document/reducers.ts +18 -3
  85. package/src/document/util.test.ts +46 -0
  86. package/src/document/util.ts +37 -1
  87. package/src/installations/installations.test-d.ts +5 -0
  88. package/src/installations/installations.ts +4 -0
  89. package/src/presence/bifurTransport.test.ts +155 -3
  90. package/src/presence/bifurTransport.ts +79 -12
  91. package/src/presence/paths.test.ts +70 -0
  92. package/src/presence/paths.ts +41 -0
  93. package/src/presence/presenceStore.test.ts +822 -2
  94. package/src/presence/presenceStore.ts +518 -102
  95. package/src/presence/types.ts +175 -3
  96. package/src/preview/types.ts +0 -25
  97. package/src/projection/projectionStore.test.ts +0 -8
  98. package/src/projection/types.ts +0 -8
  99. package/src/query/queryStore.ts +7 -6
  100. package/src/store/createActionBinder.test.ts +51 -29
  101. package/src/store/createActionBinder.ts +16 -62
  102. package/src/store/createSanityInstance.test.ts +0 -68
  103. package/src/store/createSanityInstance.ts +0 -63
  104. package/src/store/createStateSourceAction.ts +1 -1
  105. package/src/users/reducers.test.ts +0 -3
  106. package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -1147
  107. package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
  108. package/dist/_chunks-es/_internal.js +0 -134
  109. package/dist/_chunks-es/_internal.js.map +0 -1
  110. package/dist/_chunks-es/createGroqSearchFilter.js +0 -1864
  111. package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
  112. package/dist/_chunks-es/telemetryManager.js +0 -93
  113. package/dist/_chunks-es/telemetryManager.js.map +0 -1
  114. package/dist/index.d.ts.map +0 -1
  115. package/src/applications/userApplications.test.ts +0 -126
  116. package/src/applications/userApplications.ts +0 -261
  117. package/src/preview/getPreviewState.test.ts +0 -137
  118. package/src/preview/getPreviewState.ts +0 -69
  119. package/src/preview/resolvePreview.test.ts +0 -64
  120. package/src/preview/resolvePreview.ts +0 -43
@@ -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
+ }