@sanity/sdk 3.0.0-rc.1 → 3.0.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/README.md +12 -0
- package/dist/_exports/_internal.d.ts +71 -3
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +332 -24
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +65 -0
- package/dist/_exports/agent.d.ts.map +1 -0
- package/dist/_exports/agent.js +71 -0
- package/dist/_exports/agent.js.map +1 -0
- package/dist/_exports/comlink.d.ts +118 -0
- package/dist/_exports/comlink.d.ts.map +1 -0
- package/dist/_exports/comlink.js +69 -0
- package/dist/_exports/comlink.js.map +1 -0
- package/dist/authStore-C0o1NOxW.d.ts +3165 -0
- package/dist/authStore-C0o1NOxW.d.ts.map +1 -0
- package/dist/authStore-CtE9bLHW.js +754 -0
- package/dist/authStore-CtE9bLHW.js.map +1 -0
- package/dist/clientStore-BtE2-q2i.js +90 -0
- package/dist/clientStore-BtE2-q2i.js.map +1 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts +278 -0
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +1 -0
- package/dist/createStateSourceAction-BOKtqFNm.js +300 -0
- package/dist/createStateSourceAction-BOKtqFNm.js.map +1 -0
- package/dist/getEnv-D5RGkQLu.js +8 -0
- package/dist/getEnv-D5RGkQLu.js.map +1 -0
- package/dist/getNodeState-CR3__B2D.js +67 -0
- package/dist/getNodeState-CR3__B2D.js.map +1 -0
- package/dist/index.d.ts +3 -2335
- package/dist/index.js +4048 -3878
- package/dist/index.js.map +1 -1
- package/dist/logger-DAtJb5RQ.js +173 -0
- package/dist/logger-DAtJb5RQ.js.map +1 -0
- package/dist/object-CxXlSVg7.js +83 -0
- package/dist/object-CxXlSVg7.js.map +1 -0
- package/dist/queryStore-C7yMHPUI.js +1346 -0
- package/dist/queryStore-C7yMHPUI.js.map +1 -0
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js +31 -0
- package/dist/setCleanupTimeout-BiM6q1KF.js.map +1 -0
- package/dist/telemetryManager-UIEiMtC0.js +127 -0
- package/dist/telemetryManager-UIEiMtC0.js.map +1 -0
- package/dist/version-D8RP_Q59.js +9 -0
- package/dist/version-D8RP_Q59.js.map +1 -0
- package/package.json +11 -17
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/agent.ts +19 -0
- package/src/_exports/comlink.ts +16 -0
- package/src/_exports/index.ts +45 -90
- package/src/applications/applications.test-d.ts +5 -0
- package/src/applications/applications.ts +5 -2
- package/src/auth/authMode.test.ts +0 -25
- package/src/auth/authMode.ts +3 -6
- package/src/auth/authStore.test.ts +1 -1
- package/src/auth/authStore.ts +0 -1
- 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/config/sanityConfig.ts +3 -75
- package/src/datasets/datasets.test.ts +44 -9
- package/src/datasets/datasets.ts +24 -23
- package/src/document/applyDocumentActions.test.ts +8 -12
- package/src/document/documentConstants.ts +17 -0
- package/src/document/documentStore.test.ts +173 -19
- 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 +357 -0
- package/src/document/reducers.ts +125 -27
- package/src/document/util.test.ts +46 -0
- package/src/document/util.ts +37 -1
- package/src/favorites/favorites.test.ts +64 -24
- package/src/favorites/favorites.ts +73 -22
- package/src/installations/installations.test-d.ts +5 -0
- package/src/installations/installations.ts +4 -0
- package/src/presence/bifurTransport.test.ts +155 -3
- package/src/presence/bifurTransport.ts +79 -12
- package/src/presence/paths.test.ts +70 -0
- package/src/presence/paths.ts +41 -0
- package/src/presence/presenceStore.test.ts +822 -2
- package/src/presence/presenceStore.ts +518 -102
- package/src/presence/types.ts +175 -3
- package/src/preview/types.ts +0 -25
- package/src/projection/projectionStore.test.ts +0 -8
- package/src/projection/types.ts +0 -8
- package/src/query/queryStore.ts +7 -6
- package/src/store/createActionBinder.test.ts +51 -29
- package/src/store/createActionBinder.ts +16 -62
- package/src/store/createSanityInstance.test.ts +0 -68
- package/src/store/createSanityInstance.ts +0 -63
- package/src/store/createStateSourceAction.ts +1 -1
- package/src/store/fetcherStore.test.ts +23 -1
- package/src/store/fetcherStore.ts +3 -3
- package/src/users/reducers.test.ts +0 -3
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -1147
- 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 -1864
- 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
- package/src/applications/userApplications.test.ts +0 -126
- package/src/applications/userApplications.ts +0 -261
- package/src/preview/getPreviewState.test.ts +0 -137
- package/src/preview/getPreviewState.ts +0 -69
- package/src/preview/resolvePreview.test.ts +0 -64
- package/src/preview/resolvePreview.ts +0 -43
- package/src/utils/createFetcherStore.test.ts +0 -171
- package/src/utils/createFetcherStore.ts +0 -284
package/src/presence/types.ts
CHANGED
|
@@ -1,27 +1,199 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {type Path} from '@sanity/types'
|
|
2
3
|
import {type Observable} from 'rxjs'
|
|
3
4
|
|
|
5
|
+
import {type PerspectiveHandle} from '../config/sanityConfig'
|
|
4
6
|
import {type SanityUser} from '../users/types'
|
|
5
7
|
|
|
6
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* One end of a text selection: a Portable Text path plus a character offset
|
|
10
|
+
* within the span it addresses.
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export interface PresenceSelectionPoint {
|
|
14
|
+
path: Path
|
|
15
|
+
offset: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A Portable Text caret or selected range, as exchanged with the Studio.
|
|
20
|
+
*
|
|
21
|
+
* Structurally identical to `EditorSelection` from `@portabletext/editor`, and
|
|
22
|
+
* declared separately so that `@sanity/sdk` stays free of React dependencies. A
|
|
23
|
+
* value from the editor can be passed straight through, and vice versa.
|
|
24
|
+
*
|
|
25
|
+
* `offset` is a character offset within the span that `path` addresses.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export type PresenceSelection = {
|
|
29
|
+
anchor: PresenceSelectionPoint
|
|
30
|
+
focus: PresenceSelectionPoint
|
|
31
|
+
backward?: boolean
|
|
32
|
+
} | null
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Somewhere a participant is, as it arrives over the wire.
|
|
36
|
+
*
|
|
37
|
+
* Prefer {@link DocumentPresence} from `getDocumentPresence`, which is scoped to one
|
|
38
|
+
* document, flattened for rendering, and types `path` accurately.
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
7
41
|
export interface PresenceLocation {
|
|
8
42
|
type: 'document'
|
|
43
|
+
/**
|
|
44
|
+
* The specific document the participant is in, so a draft, published, or version
|
|
45
|
+
* id rather than a bare document id.
|
|
46
|
+
*/
|
|
9
47
|
documentId: string
|
|
48
|
+
/**
|
|
49
|
+
* The focused field path, empty when they are in the document but not a field.
|
|
50
|
+
*
|
|
51
|
+
* Declared as `string[]`, but segments can also be numbers or `{_key: string}`
|
|
52
|
+
* objects when the focus is inside an array or a Portable Text field, because
|
|
53
|
+
* that is what other clients send. Guard for those rather than assuming strings.
|
|
54
|
+
* Widening the declared type is a breaking change, so it waits for the next major;
|
|
55
|
+
* {@link DocumentPresence.path} is typed correctly today.
|
|
56
|
+
*/
|
|
10
57
|
path: string[]
|
|
58
|
+
/**
|
|
59
|
+
* When the participant said they were last active, on *their* clock.
|
|
60
|
+
*
|
|
61
|
+
* Fine for display, unreliable for deciding whether someone is still here: it is
|
|
62
|
+
* subject to clock skew between machines. Presence already drops participants that
|
|
63
|
+
* stop announcing, so a session appearing here is one the SDK still considers
|
|
64
|
+
* live.
|
|
65
|
+
*/
|
|
11
66
|
lastActiveAt: string
|
|
67
|
+
/** A Portable Text caret, when the focused field is a Portable Text field. */
|
|
68
|
+
selection?: PresenceSelection
|
|
12
69
|
}
|
|
13
70
|
|
|
14
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* A location as it travels over the wire. Identical in shape to
|
|
73
|
+
* {@link PresenceLocation}, except `path` is a full `Path`, which is what is
|
|
74
|
+
* really exchanged. See the note on {@link PresenceLocation.path}.
|
|
75
|
+
*
|
|
76
|
+
* Internal: nothing in the public API accepts or returns this. Apps describe
|
|
77
|
+
* where they are with {@link ReportPresenceOptions} and read presence as
|
|
78
|
+
* {@link PresenceLocation}.
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export interface WirePresenceLocation extends Omit<PresenceLocation, 'path'> {
|
|
82
|
+
path: Path
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Which specific document a presence id resolves to.
|
|
87
|
+
*
|
|
88
|
+
* Presence compares exact document ids, so a client in a draft and a client in the
|
|
89
|
+
* published document are in different places even for the same `documentId`. Pass
|
|
90
|
+
* the perspective you are editing under and the SDK resolves it; the React hooks
|
|
91
|
+
* take it from `ResourceProvider` when you do not pass one.
|
|
92
|
+
* @public
|
|
93
|
+
*/
|
|
94
|
+
export interface PresencePerspectiveOptions extends PerspectiveHandle {
|
|
95
|
+
/** Live-edit documents have no draft, so presence resolves to the published id. */
|
|
96
|
+
liveEdit?: boolean
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What an app reports about where the current user is.
|
|
101
|
+
*
|
|
102
|
+
* Omit `path` for document-level presence, or pass one for field-level presence.
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
export interface ReportPresenceOptions extends PresencePerspectiveOptions {
|
|
106
|
+
/**
|
|
107
|
+
* The document the user is in.
|
|
108
|
+
*
|
|
109
|
+
* Pass the plain document id; the perspective decides which specific document that
|
|
110
|
+
* resolves to. An already-specific id is accepted and re-resolved, so passing
|
|
111
|
+
* `drafts.abc` while the perspective is `published` reports the published document.
|
|
112
|
+
*/
|
|
113
|
+
documentId: string
|
|
114
|
+
/**
|
|
115
|
+
* The focused field path. Supports keyed and numeric segments, so array items
|
|
116
|
+
* and Portable Text spans can be addressed.
|
|
117
|
+
*/
|
|
118
|
+
path?: Path
|
|
119
|
+
/** A Portable Text caret, when the focused field is a Portable Text field. */
|
|
120
|
+
selection?: PresenceSelection
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One participant's session and everywhere it currently is.
|
|
125
|
+
*
|
|
126
|
+
* Keyed by session, not by user: the same person in two tabs is two entries with
|
|
127
|
+
* the same `user`.
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
15
130
|
export interface UserPresence {
|
|
16
131
|
user: SanityUser
|
|
17
132
|
locations: PresenceLocation[]
|
|
133
|
+
/** Identifies one tab. Stable while that tab is open, and not reused. */
|
|
134
|
+
sessionId: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* One participant at one location within a single document, flattened so it can
|
|
139
|
+
* be rendered directly against a field.
|
|
140
|
+
*
|
|
141
|
+
* Unlike {@link PresenceLocation.path}, `path` here is typed as a full `Path`, so
|
|
142
|
+
* keyed and numeric segments are described rather than cast away.
|
|
143
|
+
* @beta
|
|
144
|
+
*/
|
|
145
|
+
export interface DocumentPresence {
|
|
146
|
+
user: SanityUser
|
|
147
|
+
/** Identifies one tab, so the same person in two tabs appears twice. */
|
|
18
148
|
sessionId: string
|
|
149
|
+
/**
|
|
150
|
+
* The specific document id this participant is in: a draft, published, or
|
|
151
|
+
* version id, depending on which perspective they are editing.
|
|
152
|
+
*/
|
|
153
|
+
documentId: string
|
|
154
|
+
/** The focused field path. Empty when they are in the document but no field. */
|
|
155
|
+
path: Path
|
|
156
|
+
/**
|
|
157
|
+
* When they said they were last active, on *their* clock. Fine for display,
|
|
158
|
+
* unreliable for liveness. See {@link PresenceLocation.lastActiveAt}.
|
|
159
|
+
*/
|
|
160
|
+
lastActiveAt: string
|
|
161
|
+
/** The Portable Text caret, when they are focused in a Portable Text field. */
|
|
162
|
+
selection?: PresenceSelection
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @beta */
|
|
166
|
+
export interface DocumentPresenceOptions extends PresencePerspectiveOptions {
|
|
167
|
+
/**
|
|
168
|
+
* The document to look at. Resolved through the perspective exactly as
|
|
169
|
+
* {@link ReportPresenceOptions.documentId} is, so reads match writes.
|
|
170
|
+
*/
|
|
171
|
+
documentId: string
|
|
172
|
+
/**
|
|
173
|
+
* Narrows to participants at or below this field path. Omit it for everyone in
|
|
174
|
+
* the document.
|
|
175
|
+
*/
|
|
176
|
+
path?: Path
|
|
177
|
+
/**
|
|
178
|
+
* By default a draft, its published version, and any release versions are
|
|
179
|
+
* treated as the same document, which is what document lists want. Set this to
|
|
180
|
+
* compare ids exactly, which is what field-level indicators want so that a
|
|
181
|
+
* draft and a release version do not bleed into each other.
|
|
182
|
+
*/
|
|
183
|
+
excludeVersions?: boolean
|
|
19
184
|
}
|
|
20
185
|
|
|
21
186
|
/** @public */
|
|
22
187
|
export type PresenceTransport = [
|
|
23
188
|
incomingEvents$: Observable<TransportEvent>,
|
|
24
189
|
dispatchMessage: (message: TransportMessage) => Observable<void>,
|
|
190
|
+
/**
|
|
191
|
+
* Emits an incrementing generation number each time the connection becomes
|
|
192
|
+
* live. Reconnects with backoff, and subscribing keeps the connection alive.
|
|
193
|
+
*/
|
|
194
|
+
connections$: Observable<number>,
|
|
195
|
+
/** Emits when the page is going away, so a disconnect can be announced. */
|
|
196
|
+
unload$: Observable<void>,
|
|
25
197
|
]
|
|
26
198
|
|
|
27
199
|
/** @public */
|
|
@@ -54,7 +226,7 @@ export interface DisconnectEvent {
|
|
|
54
226
|
/** @public */
|
|
55
227
|
export type TransportMessage =
|
|
56
228
|
| {type: 'rollCall'}
|
|
57
|
-
| {type: 'state'; locations:
|
|
229
|
+
| {type: 'state'; locations: WirePresenceLocation[]}
|
|
58
230
|
| {type: 'disconnect'}
|
|
59
231
|
|
|
60
232
|
/** @public */
|
package/src/preview/types.ts
CHANGED
|
@@ -59,28 +59,3 @@ export interface PreviewValue {
|
|
|
59
59
|
lastEditedDraftAt?: string
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Represents the current state of a preview value along with a flag indicating whether
|
|
65
|
-
* the preview data is still being fetched or is fully resolved.
|
|
66
|
-
*
|
|
67
|
-
* The tuple contains a preview value or null, and a boolean indicating if the data is
|
|
68
|
-
* pending. A `true` value means a fetch is ongoing; `false` indicates that the
|
|
69
|
-
* currently provided preview value is up-to-date.
|
|
70
|
-
*
|
|
71
|
-
* @public
|
|
72
|
-
*/
|
|
73
|
-
export type ValuePending<T> = {
|
|
74
|
-
data: T | null
|
|
75
|
-
isPending: boolean
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @public
|
|
80
|
-
* @deprecated This interface is kept for backwards compatibility but is no longer used internally.
|
|
81
|
-
* Preview state is now stored in the projection store.
|
|
82
|
-
*/
|
|
83
|
-
export interface PreviewStoreState {
|
|
84
|
-
values: {[TDocumentId in string]?: ValuePending<PreviewValue>}
|
|
85
|
-
subscriptions: {[TDocumentId in string]?: {[TSubscriptionId in string]?: true}}
|
|
86
|
-
}
|
|
@@ -6,14 +6,6 @@ import {createStoreInstance} from '../store/createStoreInstance'
|
|
|
6
6
|
import {projectionStore} from './projectionStore'
|
|
7
7
|
import {subscribeToStateAndFetchBatches} from './subscribeToStateAndFetchBatches'
|
|
8
8
|
|
|
9
|
-
// Mock the module with a factory function
|
|
10
|
-
vi.mock('../common/createLiveEventSubscriber', () => {
|
|
11
|
-
const mockLiveSubscriber = vi.fn()
|
|
12
|
-
return {
|
|
13
|
-
createLiveEventSubscriber: vi.fn(() => mockLiveSubscriber),
|
|
14
|
-
}
|
|
15
|
-
})
|
|
16
|
-
|
|
17
9
|
vi.mock('./subscribeToStateAndFetchBatches')
|
|
18
10
|
|
|
19
11
|
describe('projectionStore', () => {
|
package/src/projection/types.ts
CHANGED
|
@@ -11,14 +11,6 @@ export interface DocumentProjectionValues<TValue extends object = object> {
|
|
|
11
11
|
[projectionHash: string]: ProjectionValuePending<TValue>
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* @public
|
|
16
|
-
* @deprecated
|
|
17
|
-
* Template literals are a bit too limited, so this type is deprecated.
|
|
18
|
-
* Use `string` instead. Projection strings are validated at runtime.
|
|
19
|
-
*/
|
|
20
|
-
export type ValidProjection = string
|
|
21
|
-
|
|
22
14
|
export interface DocumentProjections {
|
|
23
15
|
[projectionHash: string]: string
|
|
24
16
|
}
|
package/src/query/queryStore.ts
CHANGED
|
@@ -80,9 +80,10 @@ export interface ResolveQueryOptions<
|
|
|
80
80
|
|
|
81
81
|
const EMPTY_ARRAY: never[] = []
|
|
82
82
|
|
|
83
|
-
/** @
|
|
84
|
-
export const getQueryKey = (options: QueryOptions): string =>
|
|
85
|
-
|
|
83
|
+
/** @internal */
|
|
84
|
+
export const getQueryKey = (instance: SanityInstance, options: QueryOptions): string =>
|
|
85
|
+
JSON.stringify(normalizeOptionsWithPerspective(instance, options))
|
|
86
|
+
/** @internal */
|
|
86
87
|
export const parseQueryKey = (key: string): QueryOptions => JSON.parse(key)
|
|
87
88
|
|
|
88
89
|
/**
|
|
@@ -302,14 +303,14 @@ const _getQueryState = bindActionByResource(
|
|
|
302
303
|
createStateSourceAction({
|
|
303
304
|
selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
|
|
304
305
|
if (state.error) throw state.error
|
|
305
|
-
const key = getQueryKey(
|
|
306
|
+
const key = getQueryKey(instance, options)
|
|
306
307
|
const queryState = state.queries[key]
|
|
307
308
|
if (queryState?.error) throw queryState.error
|
|
308
309
|
return queryState?.result
|
|
309
310
|
},
|
|
310
311
|
onSubscribe: ({state, instance}, options: QueryOptions) => {
|
|
311
312
|
const subscriptionId = insecureRandomId()
|
|
312
|
-
const key = getQueryKey(
|
|
313
|
+
const key = getQueryKey(instance, options)
|
|
313
314
|
|
|
314
315
|
state.set('addSubscriber', addSubscriber(key, subscriptionId))
|
|
315
316
|
|
|
@@ -362,7 +363,7 @@ const _resolveQuery = bindActionByResource(
|
|
|
362
363
|
({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
|
|
363
364
|
const normalized = normalizeOptionsWithPerspective(instance, options)
|
|
364
365
|
const {getCurrent} = getQueryState(instance, normalized)
|
|
365
|
-
const key = getQueryKey(normalized)
|
|
366
|
+
const key = getQueryKey(instance, normalized)
|
|
366
367
|
|
|
367
368
|
// Hold a temporary subscription for the lifetime of this resolution so the
|
|
368
369
|
// key participates in normal cleanup. Without one, a fetch that errors
|
|
@@ -2,7 +2,6 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import {type DocumentResource} from '../config/sanityConfig'
|
|
4
4
|
import {
|
|
5
|
-
bindActionByDataset,
|
|
6
5
|
bindActionByResource,
|
|
7
6
|
bindActionByResourceAndPerspective,
|
|
8
7
|
bindActionGlobally,
|
|
@@ -96,34 +95,6 @@ describe('createActionBinder', () => {
|
|
|
96
95
|
})
|
|
97
96
|
})
|
|
98
97
|
|
|
99
|
-
describe('bindActionByDataset', () => {
|
|
100
|
-
it('should work correctly when projectId and dataset are provided', () => {
|
|
101
|
-
const storeDefinition = {
|
|
102
|
-
name: 'DSStore',
|
|
103
|
-
getInitialState: () => ({counter: 0}),
|
|
104
|
-
}
|
|
105
|
-
const action = vi.fn((_context, {value}: {value: string}) => value)
|
|
106
|
-
const boundAction = bindActionByDataset(storeDefinition, action)
|
|
107
|
-
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
108
|
-
const result = boundAction(instance, {value: 'hello'})
|
|
109
|
-
expect(result).toBe('hello')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('should throw an error if projectId or dataset is missing', () => {
|
|
113
|
-
const storeDefinition = {
|
|
114
|
-
name: 'DSStore',
|
|
115
|
-
getInitialState: () => ({counter: 0}),
|
|
116
|
-
}
|
|
117
|
-
const action = vi.fn((_context, _?) => 'fail')
|
|
118
|
-
const boundAction = bindActionByDataset(storeDefinition, action)
|
|
119
|
-
// Instance with missing dataset
|
|
120
|
-
const instance = createSanityInstance({projectId: 'proj1', dataset: ''})
|
|
121
|
-
expect(() => boundAction(instance)).toThrow(
|
|
122
|
-
'This API requires a project ID and dataset configured.',
|
|
123
|
-
)
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
98
|
describe('bindActionGlobally', () => {
|
|
128
99
|
it('should work correctly ignoring config in key generation', () => {
|
|
129
100
|
const storeDefinition = {
|
|
@@ -206,6 +177,57 @@ describe('bindActionByResource', () => {
|
|
|
206
177
|
})
|
|
207
178
|
})
|
|
208
179
|
|
|
180
|
+
describe('instance default resource', () => {
|
|
181
|
+
const storeDefinition = {
|
|
182
|
+
name: 'DefaultResourceStore',
|
|
183
|
+
getInitialState: () => ({counter: 0}),
|
|
184
|
+
}
|
|
185
|
+
const whichResource = bindActionByResource(storeDefinition, ({key}) => key)
|
|
186
|
+
|
|
187
|
+
it('uses config.resource when no resource is passed', () => {
|
|
188
|
+
const instance = createSanityInstance({resource: {projectId: 'p', dataset: 'd'}})
|
|
189
|
+
expect(whichResource(instance, {})).toMatchObject({
|
|
190
|
+
name: 'p.d',
|
|
191
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
192
|
+
})
|
|
193
|
+
instance.dispose()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('supports a media library as the instance default', () => {
|
|
197
|
+
const instance = createSanityInstance({resource: {mediaLibraryId: 'ml-123'}})
|
|
198
|
+
expect(whichResource(instance, {})).toMatchObject({
|
|
199
|
+
name: 'media-library:ml-123',
|
|
200
|
+
resource: {mediaLibraryId: 'ml-123'},
|
|
201
|
+
})
|
|
202
|
+
instance.dispose()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('supports a canvas as the instance default', () => {
|
|
206
|
+
const instance = createSanityInstance({resource: {canvasId: 'c-1'}})
|
|
207
|
+
expect(whichResource(instance, {})).toMatchObject({
|
|
208
|
+
name: 'canvas:c-1',
|
|
209
|
+
resource: {canvasId: 'c-1'},
|
|
210
|
+
})
|
|
211
|
+
instance.dispose()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('lets a per-call resource override the instance default', () => {
|
|
215
|
+
const instance = createSanityInstance({resource: {mediaLibraryId: 'ml-123'}})
|
|
216
|
+
expect(whichResource(instance, {resource: {projectId: 'x', dataset: 'y'}})).toMatchObject({
|
|
217
|
+
resource: {projectId: 'x', dataset: 'y'},
|
|
218
|
+
})
|
|
219
|
+
instance.dispose()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('still accepts the projectId/dataset shorthand', () => {
|
|
223
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
224
|
+
expect(whichResource(instance, {})).toMatchObject({
|
|
225
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
226
|
+
})
|
|
227
|
+
instance.dispose()
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
209
231
|
describe('bindActionByResourceAndPerspective', () => {
|
|
210
232
|
it('should throw an error when provided an invalid resource', () => {
|
|
211
233
|
const storeDefinition = {
|
|
@@ -21,12 +21,6 @@ export interface BoundResourceKey {
|
|
|
21
21
|
export interface BoundPerspectiveKey extends BoundResourceKey {
|
|
22
22
|
perspective: ClientPerspective | ReleasePerspective
|
|
23
23
|
}
|
|
24
|
-
interface BoundDatasetKey {
|
|
25
|
-
name: string
|
|
26
|
-
projectId: string
|
|
27
|
-
dataset: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
/**
|
|
31
25
|
* Defines a store action that operates on a specific state type
|
|
32
26
|
*/
|
|
@@ -123,70 +117,29 @@ export function createActionBinder<
|
|
|
123
117
|
}
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
/**
|
|
127
|
-
* Binds an action to a store that's scoped to a specific project and dataset
|
|
128
|
-
*
|
|
129
|
-
* @remarks
|
|
130
|
-
* This creates actions that operate on state isolated to a specific projectId and dataset.
|
|
131
|
-
* Different project/dataset combinations will have separate states.
|
|
132
|
-
*
|
|
133
|
-
* @throws Error if projectId or dataset is missing from the Sanity instance config
|
|
134
|
-
*
|
|
135
|
-
* @example
|
|
136
|
-
* ```ts
|
|
137
|
-
* // Define a store
|
|
138
|
-
* const documentStore = defineStore<DocumentState>({
|
|
139
|
-
* name: 'Document',
|
|
140
|
-
* getInitialState: () => ({ documents: {} }),
|
|
141
|
-
* // ...
|
|
142
|
-
* })
|
|
143
|
-
*
|
|
144
|
-
* // Create dataset-specific actions
|
|
145
|
-
* export const fetchDocument = bindActionByDataset(
|
|
146
|
-
* documentStore,
|
|
147
|
-
* ({instance, state}, documentId) => {
|
|
148
|
-
* // This state is isolated to the specific project/dataset
|
|
149
|
-
* // ...fetch logic...
|
|
150
|
-
* }
|
|
151
|
-
* )
|
|
152
|
-
*
|
|
153
|
-
* // Usage
|
|
154
|
-
* fetchDocument(sanityInstance, 'doc123')
|
|
155
|
-
* ```
|
|
156
|
-
*/
|
|
157
|
-
export const bindActionByDataset = createActionBinder<
|
|
158
|
-
BoundDatasetKey,
|
|
159
|
-
[(object & {projectId?: string; dataset?: string})?, ...unknown[]]
|
|
160
|
-
>((instance, options) => {
|
|
161
|
-
const projectId = options?.projectId ?? instance.config.projectId
|
|
162
|
-
const dataset = options?.dataset ?? instance.config.dataset
|
|
163
|
-
if (!projectId || !dataset) {
|
|
164
|
-
throw new Error('This API requires a project ID and dataset configured.')
|
|
165
|
-
}
|
|
166
|
-
return {name: `${projectId}.${dataset}`, projectId, dataset}
|
|
167
|
-
})
|
|
168
|
-
|
|
169
120
|
const createResourceKey = (
|
|
170
121
|
instance: SanityInstance,
|
|
171
122
|
resource?: DocumentResource,
|
|
172
123
|
): BoundResourceKey => {
|
|
173
124
|
let name: string | undefined
|
|
174
125
|
let resourceForKey: DocumentResource | undefined
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
name =
|
|
181
|
-
} else if (
|
|
182
|
-
name = `
|
|
126
|
+
// A per-call resource wins; otherwise use the instance's default resource.
|
|
127
|
+
const effectiveResource = resource ?? instance.config.resource
|
|
128
|
+
if (effectiveResource) {
|
|
129
|
+
resourceForKey = effectiveResource
|
|
130
|
+
if (isDatasetResource(effectiveResource)) {
|
|
131
|
+
name = `${effectiveResource.projectId}.${effectiveResource.dataset}`
|
|
132
|
+
} else if (isMediaLibraryResource(effectiveResource)) {
|
|
133
|
+
name = `media-library:${effectiveResource.mediaLibraryId}`
|
|
134
|
+
} else if (isCanvasResource(effectiveResource)) {
|
|
135
|
+
name = `canvas:${effectiveResource.canvasId}`
|
|
183
136
|
} else {
|
|
184
|
-
throw new Error(`Received invalid resource: ${JSON.stringify(
|
|
137
|
+
throw new Error(`Received invalid resource: ${JSON.stringify(effectiveResource)}`)
|
|
185
138
|
}
|
|
186
139
|
return {name, resource: resourceForKey}
|
|
187
140
|
}
|
|
188
141
|
|
|
189
|
-
//
|
|
142
|
+
// `projectId`/`dataset` on the config are shorthand for a dataset resource.
|
|
190
143
|
const {projectId, dataset} = instance.config
|
|
191
144
|
if (!projectId || !dataset) {
|
|
192
145
|
throw new Error('This API requires a project ID and dataset configured.')
|
|
@@ -199,8 +152,8 @@ const createResourceKey = (
|
|
|
199
152
|
**/
|
|
200
153
|
export const bindActionByResource = createActionBinder<
|
|
201
154
|
BoundResourceKey,
|
|
202
|
-
//
|
|
203
|
-
//
|
|
155
|
+
// `resource` is optional because it falls back to the instance config in
|
|
156
|
+
// `createResourceKey` — see the note there on why that fallback still exists.
|
|
204
157
|
[{resource?: DocumentResource}, ...unknown[]]
|
|
205
158
|
>((instance, {resource}) => {
|
|
206
159
|
return createResourceKey(instance, resource)
|
|
@@ -245,7 +198,8 @@ export const bindActionByResourceAndPerspective = createActionBinder<
|
|
|
245
198
|
[DatasetHandle, ...unknown[]]
|
|
246
199
|
>((instance, options): BoundPerspectiveKey => {
|
|
247
200
|
const {resource, perspective} = options
|
|
248
|
-
//
|
|
201
|
+
// Same trade-off as the resource fallback in `createResourceKey`: configured once on the
|
|
202
|
+
// instance rather than passed to every call. Kept for the same reason.
|
|
249
203
|
const utilizedPerspective = perspective ?? instance.config.perspective ?? 'drafts'
|
|
250
204
|
let perspectiveKey: string
|
|
251
205
|
if (isReleasePerspective(utilizedPerspective)) {
|
|
@@ -29,60 +29,6 @@ describe('createSanityInstance', () => {
|
|
|
29
29
|
expect(callback).toHaveBeenCalledTimes(1)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it('should create a child instance with merged config and correct parent', () => {
|
|
33
|
-
const parent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
34
|
-
const child = parent.createChild({dataset: 'ds2'})
|
|
35
|
-
expect(child.config).toEqual({projectId: 'proj1', dataset: 'ds2'})
|
|
36
|
-
expect(child.getParent()).toBe(parent)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('should match an instance in the hierarchy using match', () => {
|
|
40
|
-
// three-level hierarchy
|
|
41
|
-
const grandparent = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
42
|
-
const parent = grandparent.createChild({projectId: 'proj2'})
|
|
43
|
-
const child = parent.createChild({dataset: 'ds2'})
|
|
44
|
-
|
|
45
|
-
expect(child.config).toEqual({projectId: 'proj2', dataset: 'ds2'})
|
|
46
|
-
expect(parent.config).toEqual({projectId: 'proj2', dataset: 'ds1'})
|
|
47
|
-
|
|
48
|
-
expect(child.match({dataset: 'ds2'})).toBe(child)
|
|
49
|
-
expect(child.match({projectId: 'proj2'})).toBe(child)
|
|
50
|
-
expect(child.match({projectId: 'proj1'})).toBe(grandparent)
|
|
51
|
-
expect(parent.match({projectId: 'proj1'})).toBe(grandparent)
|
|
52
|
-
expect(grandparent.match({projectId: 'proj1'})).toBe(grandparent)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should match `undefined` when the desired resource ID should not be set on an instance', () => {
|
|
56
|
-
const noProjectOrDataset = createSanityInstance()
|
|
57
|
-
const noDataset = noProjectOrDataset.createChild({projectId: 'proj1'})
|
|
58
|
-
const leaf = noDataset.createChild({dataset: 'ds1'})
|
|
59
|
-
|
|
60
|
-
// no keys means anything (in this case, self) will match
|
|
61
|
-
expect(leaf.match({})).toBe(leaf)
|
|
62
|
-
|
|
63
|
-
// `[resourceId]: undefined` means match an instance with no dataset set
|
|
64
|
-
expect(leaf.match({dataset: undefined})).toBe(noDataset)
|
|
65
|
-
expect(noDataset.match({dataset: undefined})).toBe(noDataset)
|
|
66
|
-
expect(leaf.match({projectId: undefined})).toBe(noProjectOrDataset)
|
|
67
|
-
expect(noDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
|
|
68
|
-
expect(noProjectOrDataset.match({projectId: undefined})).toBe(noProjectOrDataset)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should return undefined when no match is found', () => {
|
|
72
|
-
const instance = createSanityInstance({projectId: 'proj1', dataset: 'ds1'})
|
|
73
|
-
expect(instance.match({dataset: 'non-existent'})).toBeUndefined()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('should inherit and merge auth config', () => {
|
|
77
|
-
const parent = createSanityInstance({
|
|
78
|
-
projectId: 'proj1',
|
|
79
|
-
dataset: 'ds1',
|
|
80
|
-
auth: {apiHost: 'api.sanity.work'},
|
|
81
|
-
})
|
|
82
|
-
const child = parent.createChild({auth: {token: 'my-token'}})
|
|
83
|
-
expect(child.config.auth).toEqual({apiHost: 'api.sanity.work', token: 'my-token'})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
32
|
describe('logging', () => {
|
|
87
33
|
const mockHandler: LogHandler = {
|
|
88
34
|
error: vi.fn(),
|
|
@@ -141,20 +87,6 @@ describe('createSanityInstance', () => {
|
|
|
141
87
|
)
|
|
142
88
|
})
|
|
143
89
|
|
|
144
|
-
it('should log child instance creation at debug level', () => {
|
|
145
|
-
const parent = createSanityInstance({projectId: 'parent-proj'})
|
|
146
|
-
vi.clearAllMocks() // Clear parent creation logs
|
|
147
|
-
|
|
148
|
-
parent.createChild({dataset: 'child-ds'})
|
|
149
|
-
|
|
150
|
-
expect(mockHandler.debug).toHaveBeenCalledWith(
|
|
151
|
-
expect.stringContaining('Creating child instance'),
|
|
152
|
-
expect.objectContaining({
|
|
153
|
-
overridingDataset: true,
|
|
154
|
-
}),
|
|
155
|
-
)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
90
|
it('should include instance context in logs', () => {
|
|
159
91
|
createSanityInstance({projectId: 'my-project', dataset: 'my-dataset'})
|
|
160
92
|
|