@sanity/sdk 3.0.0 → 3.2.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 +72 -19
- package/dist/_exports/_internal.d.ts.map +1 -1
- package/dist/_exports/_internal.js +130 -11
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/_exports/agent.d.ts +7 -7
- package/dist/_exports/agent.d.ts.map +1 -1
- package/dist/_exports/agent.js +1 -1
- package/dist/_exports/comlink.d.ts +9 -9
- package/dist/_exports/comlink.d.ts.map +1 -1
- package/dist/_exports/comlink.js +2 -2
- package/dist/_exports/dashboard.d.ts +2 -0
- package/dist/_exports/dashboard.js +2 -0
- package/dist/{authStore-C0o1NOxW.d.ts → authStore-BO2vch3I.d.ts} +516 -63
- package/dist/authStore-BO2vch3I.d.ts.map +1 -0
- package/dist/{authStore-CtE9bLHW.js → clientStore-BI4D8HhJ.js} +390 -15
- package/dist/clientStore-BI4D8HhJ.js.map +1 -0
- package/dist/{createSanityInstance-DUI1bGIg.d.ts → createSanityInstance-BQIqyGLX.d.ts} +17 -2
- package/dist/createSanityInstance-BQIqyGLX.d.ts.map +1 -0
- package/dist/{createStateSourceAction-BOKtqFNm.js → createStateSourceAction-DdAhEfnZ.js} +7 -3
- package/dist/createStateSourceAction-DdAhEfnZ.js.map +1 -0
- package/dist/defineProperty-Bho9Ngsi.js +31 -0
- package/dist/{getNodeState-CR3__B2D.js → getNodeState-B4nG0sjm.js} +2 -2
- package/dist/{getNodeState-CR3__B2D.js.map → getNodeState-B4nG0sjm.js.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +370 -90
- package/dist/index.js.map +1 -1
- package/dist/topicStore-Ct7IFNtY.js +565 -0
- package/dist/topicStore-Ct7IFNtY.js.map +1 -0
- package/dist/topicStore-D0WqhQGF.d.ts +435 -0
- package/dist/topicStore-D0WqhQGF.d.ts.map +1 -0
- package/dist/{queryStore-C7yMHPUI.js → version-BcRmCiYi.js} +48 -16
- package/dist/version-BcRmCiYi.js.map +1 -0
- package/package.json +19 -18
- package/src/_exports/_internal.ts +13 -0
- package/src/_exports/dashboard.ts +36 -0
- package/src/_exports/index.ts +17 -1
- package/src/access/checkPermissions.test.ts +1 -1
- package/src/access/checkPermissions.ts +1 -1
- package/src/applications/applications.test-d.ts +28 -4
- package/src/applications/applications.test.ts +4 -4
- package/src/applications/applications.ts +43 -18
- package/src/auth/authMode.test.ts +24 -0
- package/src/auth/authMode.ts +8 -3
- package/src/auth/authStore.ts +12 -0
- package/src/auth/authStrategy.ts +2 -0
- package/src/auth/handleAuthCallback.test.ts +2 -2
- package/src/auth/handleAuthCallback.ts +1 -1
- package/src/auth/logout.test.ts +46 -1
- package/src/auth/logout.ts +2 -1
- package/src/auth/oauth/oauthActions.test.ts +457 -0
- package/src/auth/oauth/oauthActions.ts +389 -0
- package/src/auth/oauth/oauthAuth.test.ts +206 -0
- package/src/auth/oauth/oauthAuth.ts +168 -0
- package/src/auth/oauth/pkce.test.ts +44 -0
- package/src/auth/oauth/pkce.ts +60 -0
- package/src/auth/oauth/types.ts +12 -0
- package/src/auth/refreshStampedToken.ts +1 -1
- package/src/auth/studioModeAuth.test.ts +1 -1
- package/src/auth/studioModeAuth.ts +1 -1
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +2 -2
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +1 -1
- package/src/comments/addonDatasetStore.test.ts +2 -2
- package/src/comments/addonDatasetStore.ts +2 -2
- package/src/comments/commentActions.ts +6 -5
- package/src/comments/commentsStore.ts +4 -4
- package/src/config/authConfig.ts +18 -0
- package/src/dashboard/messageBus/__fixtures__/test-topics.ts +18 -0
- package/src/dashboard/messageBus/bus.test.ts +1033 -0
- package/src/dashboard/messageBus/bus.ts +1246 -0
- package/src/dashboard/messageBus/store.test.ts +142 -0
- package/src/dashboard/messageBus/store.ts +74 -0
- package/src/dashboard/messageBus/topicStore.test.ts +172 -0
- package/src/dashboard/messageBus/topicStore.ts +184 -0
- package/src/dashboard/messageBus/topics.test.ts +71 -0
- package/src/dashboard/messageBus/topics.ts +260 -0
- package/src/document/actions.ts +3 -3
- package/src/document/applyDocumentActions.ts +5 -9
- package/src/document/documentStore.concurrency.test.ts +2 -1
- package/src/document/documentStore.test.ts +19 -18
- package/src/document/documentStore.ts +17 -17
- package/src/document/permissions.ts +2 -1
- package/src/document/processActions.test.ts +2 -1
- package/src/document/processMutations.ts +61 -24
- package/src/document/reducers.ts +2 -2
- package/src/installations/installations.test-d.ts +3 -3
- package/src/installations/installations.test.ts +2 -2
- package/src/installations/installations.ts +4 -21
- package/src/organization/organization.test.ts +2 -2
- package/src/organization/organization.ts +1 -1
- package/src/organizations/organizations.test.ts +2 -2
- package/src/organizations/organizations.ts +1 -1
- package/src/presence/presenceStore.test.ts +15 -22
- package/src/presence/presenceStore.ts +3 -2
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +1 -1
- package/src/projection/getProjectionState.test.ts +6 -4
- package/src/projection/getProjectionState.ts +4 -4
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projects/projects.test.ts +2 -2
- package/src/projects/projects.ts +1 -1
- package/src/query/queryStore.ts +6 -6
- package/src/store/createSanityInstance.test.ts +1 -1
- package/src/store/createSanityInstance.ts +4 -4
- package/src/store/createStoreInstance.test.ts +0 -5
- package/src/telemetry/telemetryManager.test.ts +1 -1
- package/src/telemetry/telemetryManager.ts +2 -2
- package/src/typegen/defineProjection.test.ts +17 -0
- package/src/typegen/defineProjection.ts +19 -0
- package/src/typegen/groqCompat.ts +122 -0
- package/src/typegen/resolve.ts +37 -0
- package/src/users/usersStore.test.ts +1 -1
- package/src/users/usersStore.ts +5 -5
- package/src/utils/createGroqSearchFilter.test.ts +8 -0
- package/src/utils/createGroqSearchFilter.ts +6 -3
- package/src/utils/ids.test.ts +40 -15
- package/src/utils/ids.ts +47 -2
- package/dist/authStore-C0o1NOxW.d.ts.map +0 -1
- package/dist/authStore-CtE9bLHW.js.map +0 -1
- package/dist/clientStore-BtE2-q2i.js +0 -90
- package/dist/clientStore-BtE2-q2i.js.map +0 -1
- package/dist/createSanityInstance-DUI1bGIg.d.ts.map +0 -1
- package/dist/createStateSourceAction-BOKtqFNm.js.map +0 -1
- package/dist/getEnv-D5RGkQLu.js +0 -8
- package/dist/getEnv-D5RGkQLu.js.map +0 -1
- package/dist/logger-DAtJb5RQ.js +0 -173
- package/dist/logger-DAtJb5RQ.js.map +0 -1
- package/dist/queryStore-C7yMHPUI.js.map +0 -1
- package/dist/rolldown-runtime-vyAXikos.js +0 -9
- package/dist/telemetryManager-UIEiMtC0.js +0 -127
- package/dist/telemetryManager-UIEiMtC0.js.map +0 -1
- package/dist/version-D8RP_Q59.js +0 -9
- package/dist/version-D8RP_Q59.js.map +0 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {type CurrentUser} from '@sanity/types'
|
|
2
|
+
|
|
3
|
+
import {type Application, type ApplicationInclude} from '../../applications/applications'
|
|
4
|
+
import {type OrganizationBase} from '../../organization/organization'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Identifies a module federation expose and the manifest that serves it.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface RemoteModuleRef {
|
|
11
|
+
readonly entry: string
|
|
12
|
+
readonly moduleId: string
|
|
13
|
+
readonly version: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Application types that can receive configuration modules.
|
|
18
|
+
*
|
|
19
|
+
* The dashboard supplies this value over the message bus, so any string is accepted; known
|
|
20
|
+
* types are listed for autocomplete. `& {}` keeps the literal from collapsing into `string`.
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export type ApplicationConfigAppType = 'media-library' | (string & {})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Identifies a configuration module for an application or application type.
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export interface ApplicationConfig extends RemoteModuleRef {
|
|
30
|
+
readonly appId?: Application['id']
|
|
31
|
+
readonly appType: ApplicationConfigAppType
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Declares a topic that stores and replays its current value.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export type StateTopicDef<T> = {kind: 'state'; value: T}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Declares a topic that delivers events and an optional reply.
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export type EventTopicDef<P, R = never> = {
|
|
45
|
+
kind: 'event'
|
|
46
|
+
payload: P
|
|
47
|
+
reply?: R
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Represents a successful topic value or a failed topic operation.
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export type TopicResult<T> = {ok: true; value: T} | {ok: false}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Identifies a dashboard application and a route within it.
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export type NavigationTarget = {
|
|
61
|
+
/** The application ID, or `null` for dashboard-level routes. */
|
|
62
|
+
appId: Application['id'] | null
|
|
63
|
+
/** The route path, including its query string and fragment. */
|
|
64
|
+
path: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Describes the current dashboard location and an active navigation.
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export type NavigationLocation = NavigationTarget & {
|
|
72
|
+
/** The active navigation, or `null` when navigation is idle. */
|
|
73
|
+
transition: {
|
|
74
|
+
/** Whether the navigation pushes or replaces browser history. */
|
|
75
|
+
navigationType: 'push' | 'replace'
|
|
76
|
+
/** The requested destination. */
|
|
77
|
+
to: NavigationTarget
|
|
78
|
+
} | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Declares the topics provided by the dashboard.
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export interface DashboardTopics {
|
|
86
|
+
/** The available application configuration modules. */
|
|
87
|
+
'applications.config': StateTopicDef<ApplicationConfig[] | null>
|
|
88
|
+
/** The foreground application ID, or `null` on dashboard-level routes. */
|
|
89
|
+
'applications.foreground': StateTopicDef<Application['id'] | null>
|
|
90
|
+
/** The dashboard applications available to the current user. */
|
|
91
|
+
'applications.list': StateTopicDef<TopicResult<Application<ApplicationInclude>[]> | null>
|
|
92
|
+
/**
|
|
93
|
+
* The session token for the reading connection, or `null` while signed out. The host
|
|
94
|
+
* writes it to each connection separately, so one application never sees another's token.
|
|
95
|
+
*/
|
|
96
|
+
'auth.token': StateTopicDef<string | null>
|
|
97
|
+
/** Requests a dashboard session token. */
|
|
98
|
+
'auth.token.refresh': EventTopicDef<void, string>
|
|
99
|
+
/** The current dashboard location and active navigation. */
|
|
100
|
+
'navigation.location': StateTopicDef<NavigationLocation | null>
|
|
101
|
+
/**
|
|
102
|
+
* Requests navigation and replies when the location commits.
|
|
103
|
+
*
|
|
104
|
+
* `ok: false` reasons:
|
|
105
|
+
* - `not-navigable`: the URL cannot be handled by a dashboard application
|
|
106
|
+
* - `interrupted`: another navigation superseded the request
|
|
107
|
+
* - `failed`: the router rejected or did not commit the request
|
|
108
|
+
*/
|
|
109
|
+
'navigation.location.update': EventTopicDef<
|
|
110
|
+
{
|
|
111
|
+
url: string
|
|
112
|
+
history?: 'push' | 'replace'
|
|
113
|
+
},
|
|
114
|
+
{ok: true} | {ok: false; reason: 'not-navigable' | 'interrupted' | 'failed'}
|
|
115
|
+
>
|
|
116
|
+
/** The current organization, or `null` without an active organization. */
|
|
117
|
+
'organizations.current': StateTopicDef<Pick<OrganizationBase, 'id' | 'name' | 'slug'> | null>
|
|
118
|
+
/** The open panel, its application, display mode, and optional width in pixels. */
|
|
119
|
+
'panels.mode': StateTopicDef<TopicResult<
|
|
120
|
+
| {appId: string; name: string; mode: 'aside'; size?: number}
|
|
121
|
+
| {appId: string; name: string; mode: 'full'}
|
|
122
|
+
| null
|
|
123
|
+
> | null>
|
|
124
|
+
/** Opens, updates, resizes, or closes an application's panel. */
|
|
125
|
+
'panels.mode.set': EventTopicDef<
|
|
126
|
+
{name: string; mode: 'aside' | 'full'} | {name: string; size: number} | null
|
|
127
|
+
>
|
|
128
|
+
/** The resolved dashboard color scheme. */
|
|
129
|
+
'preferences.color-scheme': StateTopicDef<'light' | 'dark'>
|
|
130
|
+
/** Whether the dashboard dock is pinned open. */
|
|
131
|
+
'preferences.dock-locked': StateTopicDef<boolean>
|
|
132
|
+
/** The signed-in user, or `null` while signed out. */
|
|
133
|
+
'users.current': StateTopicDef<Pick<CurrentUser, 'id' | 'name' | 'email' | 'profileImage'> | null>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Declares every topic available through the message bus.
|
|
138
|
+
* @public
|
|
139
|
+
*/
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- Declaration merging extends the SDK manifest.
|
|
141
|
+
export interface Topics extends DashboardTopics {}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Every declared topic name.
|
|
145
|
+
* @public
|
|
146
|
+
*/
|
|
147
|
+
export type TopicName = keyof Topics
|
|
148
|
+
|
|
149
|
+
type StateTopicsOf<T> = {
|
|
150
|
+
[K in keyof T]: T[K] extends {kind: 'state'} ? K : never
|
|
151
|
+
}[keyof T]
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Names of all declared state topics.
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
export type StateTopic = StateTopicsOf<Topics>
|
|
158
|
+
|
|
159
|
+
type TopicOwnership = {readonly type: 'same_app'} | {readonly type: 'any_app'}
|
|
160
|
+
|
|
161
|
+
// State is only ever written by the host, one connection at a time, so it carries no ownership.
|
|
162
|
+
type TopicManifestEntry<T> = T extends {kind: 'state'; value: infer V}
|
|
163
|
+
? {readonly kind: 'state'; readonly seed: V | undefined}
|
|
164
|
+
: {readonly kind: 'event'; readonly ownership: TopicOwnership}
|
|
165
|
+
|
|
166
|
+
// The value a connection's copy of the topic starts with; `undefined` means unpublished.
|
|
167
|
+
const stateTopic = <const V>(seed: V) => ({kind: 'state', seed}) as const
|
|
168
|
+
|
|
169
|
+
// `same_app` restricts responding to the application that installed the bus.
|
|
170
|
+
const dashboardEvent = {kind: 'event', ownership: {type: 'same_app'}} as const
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Defines the runtime kind, ownership, and initial value of dashboard topics.
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
export const DASHBOARD_TOPIC_MANIFEST: {
|
|
177
|
+
readonly [K in keyof DashboardTopics]: TopicManifestEntry<DashboardTopics[K]>
|
|
178
|
+
} = {
|
|
179
|
+
'applications.config': stateTopic(undefined),
|
|
180
|
+
'applications.foreground': stateTopic(undefined),
|
|
181
|
+
'applications.list': stateTopic(undefined),
|
|
182
|
+
'auth.token': stateTopic(undefined),
|
|
183
|
+
'auth.token.refresh': dashboardEvent,
|
|
184
|
+
'navigation.location': stateTopic(undefined),
|
|
185
|
+
'navigation.location.update': dashboardEvent,
|
|
186
|
+
'organizations.current': stateTopic(undefined),
|
|
187
|
+
'panels.mode': stateTopic({ok: true, value: null}),
|
|
188
|
+
'panels.mode.set': dashboardEvent,
|
|
189
|
+
'preferences.color-scheme': stateTopic(undefined),
|
|
190
|
+
'preferences.dock-locked': stateTopic(undefined),
|
|
191
|
+
'users.current': stateTopic(undefined),
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Defines the runtime manifest accepted from any message bus version.
|
|
196
|
+
* @internal
|
|
197
|
+
*/
|
|
198
|
+
export type TopicManifest = Readonly<
|
|
199
|
+
Record<
|
|
200
|
+
string,
|
|
201
|
+
| {readonly kind: 'state'; readonly seed: unknown}
|
|
202
|
+
| {readonly kind: 'event'; readonly ownership: TopicOwnership}
|
|
203
|
+
>
|
|
204
|
+
>
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Names of all declared event topics.
|
|
208
|
+
* @public
|
|
209
|
+
*/
|
|
210
|
+
export type EventTopic = {
|
|
211
|
+
[K in TopicName]: Topics[K] extends {kind: 'event'} ? K : never
|
|
212
|
+
}[TopicName]
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The value type of a state topic.
|
|
216
|
+
* @public
|
|
217
|
+
*/
|
|
218
|
+
export type ValueOf<K extends StateTopic> = Topics[K] extends StateTopicDef<infer T> ? T : never
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The payload type of an event topic.
|
|
222
|
+
* @public
|
|
223
|
+
*/
|
|
224
|
+
export type PayloadOf<K extends EventTopic> =
|
|
225
|
+
Topics[K] extends EventTopicDef<infer P, infer _R> ? P : never
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The reply type of an event topic (`never` if it declares none).
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
export type ReplyOf<K extends EventTopic> =
|
|
232
|
+
Topics[K] extends EventTopicDef<infer _P, infer R> ? R : never
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Converts a topic value between 2 adjacent versions.
|
|
236
|
+
* @internal
|
|
237
|
+
*/
|
|
238
|
+
export interface TopicMigration {
|
|
239
|
+
/** The older version. */
|
|
240
|
+
readonly from: number
|
|
241
|
+
/** The newer version. */
|
|
242
|
+
readonly to: number
|
|
243
|
+
/** Converts an older state value or event payload to the newer version. */
|
|
244
|
+
up(older: unknown): unknown
|
|
245
|
+
/** Converts a newer state value or event payload to the older version. */
|
|
246
|
+
down(newer: unknown): unknown
|
|
247
|
+
/** Converts event replies between these versions. */
|
|
248
|
+
readonly reply?: {
|
|
249
|
+
/** Converts an older reply to the newer version. */
|
|
250
|
+
up(older: unknown): unknown
|
|
251
|
+
/** Converts a newer reply to the older version. */
|
|
252
|
+
down(newer: unknown): unknown
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Defines the bundled migration chain for each topic.
|
|
258
|
+
* @internal
|
|
259
|
+
*/
|
|
260
|
+
export const topicMigrations: Partial<Record<TopicName, readonly TopicMigration[]>> = {}
|
package/src/document/actions.ts
CHANGED
|
@@ -2,13 +2,13 @@ import {type ReleaseDocument} from '@sanity/client'
|
|
|
2
2
|
import {SanityEncoder} from '@sanity/mutate'
|
|
3
3
|
import {type PatchMutation as SanityMutatePatchMutation} from '@sanity/mutate/_unstable_store'
|
|
4
4
|
import {type PatchMutation, type PatchOperations} from '@sanity/types'
|
|
5
|
-
import {type SanityDocument} from 'groq'
|
|
6
5
|
|
|
7
6
|
import {
|
|
8
7
|
type DocumentHandle,
|
|
9
8
|
type DocumentTypeHandle,
|
|
10
9
|
type ReleaseHandle,
|
|
11
10
|
} from '../config/sanityConfig'
|
|
11
|
+
import {type ResolveDocument} from '../typegen/resolve'
|
|
12
12
|
import {getEffectiveDocumentId} from './util'
|
|
13
13
|
|
|
14
14
|
const isSanityMutatePatch = (value: unknown): value is SanityMutatePatchMutation => {
|
|
@@ -37,7 +37,7 @@ export interface CreateDocumentAction<
|
|
|
37
37
|
*/
|
|
38
38
|
initialValue?: Partial<
|
|
39
39
|
Omit<
|
|
40
|
-
|
|
40
|
+
ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>,
|
|
41
41
|
'_id' | '_type' | '_rev' | '_createdAt' | '_updatedAt'
|
|
42
42
|
>
|
|
43
43
|
>
|
|
@@ -166,7 +166,7 @@ export function createDocument<
|
|
|
166
166
|
doc: DocumentTypeHandle<TDocumentType, TDataset, TProjectId>,
|
|
167
167
|
initialValue?: Partial<
|
|
168
168
|
Omit<
|
|
169
|
-
|
|
169
|
+
ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>,
|
|
170
170
|
'_id' | '_type' | '_rev' | '_createdAt' | '_updatedAt'
|
|
171
171
|
>
|
|
172
172
|
>,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {type SanityDocument} from 'groq'
|
|
2
1
|
import {distinctUntilChanged, filter, first, firstValueFrom, map, race} from 'rxjs'
|
|
3
2
|
|
|
4
3
|
import {type DocumentResource} from '../config/sanityConfig'
|
|
5
4
|
import {bindActionByResource} from '../store/createActionBinder'
|
|
6
5
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
6
|
import {type StoreContext} from '../store/defineStore'
|
|
7
|
+
import {type ResolveDocument} from '../typegen/resolve'
|
|
8
|
+
import {randomUuid} from '../utils/ids'
|
|
8
9
|
import {type Action} from './actions'
|
|
9
10
|
import {documentStore, type DocumentStoreState} from './documentStore'
|
|
10
11
|
import {type DocumentTransactionSubmissionResult} from './events'
|
|
@@ -13,7 +14,7 @@ import {type AppliedTransaction, type QueuedTransaction, queueTransaction} from
|
|
|
13
14
|
import {normalizeActionsForResource} from './resourceRules'
|
|
14
15
|
|
|
15
16
|
/** @beta */
|
|
16
|
-
export interface ActionsResult<TDocument extends
|
|
17
|
+
export interface ActionsResult<TDocument extends ResolveDocument = ResolveDocument> {
|
|
17
18
|
transactionId: string
|
|
18
19
|
documents: DocumentSet<TDocument>
|
|
19
20
|
previous: DocumentSet<TDocument>
|
|
@@ -55,7 +56,7 @@ export function applyDocumentActions<
|
|
|
55
56
|
>(
|
|
56
57
|
instance: SanityInstance,
|
|
57
58
|
options: ApplyDocumentActionsOptions,
|
|
58
|
-
): Promise<ActionsResult<
|
|
59
|
+
): Promise<ActionsResult<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
59
60
|
/** @beta */
|
|
60
61
|
export function applyDocumentActions(
|
|
61
62
|
instance: SanityInstance,
|
|
@@ -74,12 +75,7 @@ const boundApplyDocumentActions = bindActionByResource(documentStore, _applyDocu
|
|
|
74
75
|
/** @internal */
|
|
75
76
|
async function _applyDocumentActions(
|
|
76
77
|
{state}: StoreContext<DocumentStoreState>,
|
|
77
|
-
{
|
|
78
|
-
actions,
|
|
79
|
-
resource,
|
|
80
|
-
transactionId = crypto.randomUUID(),
|
|
81
|
-
disableBatching,
|
|
82
|
-
}: ApplyDocumentActionsOptions,
|
|
78
|
+
{actions, resource, transactionId = randomUuid(), disableBatching}: ApplyDocumentActionsOptions,
|
|
83
79
|
): Promise<ActionsResult> {
|
|
84
80
|
const {events} = state.get()
|
|
85
81
|
|
|
@@ -44,6 +44,7 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
44
44
|
import {getClientState} from '../client/clientStore'
|
|
45
45
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
46
46
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
47
|
+
import {randomUuid} from '../utils/ids'
|
|
47
48
|
import {editDocument, publishDocument} from './actions'
|
|
48
49
|
import {applyDocumentActions} from './applyDocumentActions'
|
|
49
50
|
import {getDocumentState, getDocumentSyncStatus} from './documentStore'
|
|
@@ -273,7 +274,7 @@ function createMockClient(server: MockContentLake): SanityClient {
|
|
|
273
274
|
|
|
274
275
|
const action = vi.fn(async (input: HttpAction | HttpAction[], options?: BaseActionOptions) => {
|
|
275
276
|
const actions = Array.isArray(input) ? input : [input]
|
|
276
|
-
return server.submitActions(actions, options?.transactionId ??
|
|
277
|
+
return server.submitActions(actions, options?.transactionId ?? randomUuid())
|
|
277
278
|
})
|
|
278
279
|
|
|
279
280
|
const request = vi.fn(async () => {
|
|
@@ -36,6 +36,7 @@ import {getClientState} from '../client/clientStore'
|
|
|
36
36
|
import {createDocumentHandle} from '../config/handles'
|
|
37
37
|
import {createSanityInstance, type SanityInstance} from '../store/createSanityInstance'
|
|
38
38
|
import {type StateSource} from '../store/createStateSourceAction'
|
|
39
|
+
import {randomUuid} from '../utils/ids'
|
|
39
40
|
import {
|
|
40
41
|
createDocument,
|
|
41
42
|
deleteDocument,
|
|
@@ -369,7 +370,7 @@ it('labels an echo that arrives after the last subscriber left as its own', asyn
|
|
|
369
370
|
// and out of band, which is the ordering the real API produces
|
|
370
371
|
const clientActionMockImplementation = vi.mocked(client.action).getMockImplementation()!
|
|
371
372
|
vi.mocked(client.action).mockImplementation(
|
|
372
|
-
async (_input, {transactionId =
|
|
373
|
+
async (_input, {transactionId = randomUuid()} = {}) => ({transactionId}),
|
|
373
374
|
)
|
|
374
375
|
|
|
375
376
|
const patchEvents: DocumentRemotePatchesEvent[] = []
|
|
@@ -671,7 +672,7 @@ it('fetches documents if there are no active subscriptions for the actions appli
|
|
|
671
672
|
})
|
|
672
673
|
|
|
673
674
|
it('batches edit transaction into one outgoing transaction', async () => {
|
|
674
|
-
const doc = createDocumentHandle({documentId:
|
|
675
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
675
676
|
|
|
676
677
|
const unsubscribe = getDocumentState(instance, doc).subscribe()
|
|
677
678
|
|
|
@@ -747,7 +748,7 @@ it('submits liveEdit document edits through observable.mutate', async () => {
|
|
|
747
748
|
})
|
|
748
749
|
|
|
749
750
|
it('provides the consistency status via `getDocumentSyncStatus`', async () => {
|
|
750
|
-
const doc = createDocumentHandle({documentId:
|
|
751
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
751
752
|
|
|
752
753
|
const syncStatus = getDocumentSyncStatus(instance, doc)
|
|
753
754
|
expect(syncStatus.getCurrent()).toBeUndefined()
|
|
@@ -802,7 +803,7 @@ it('reverts failed outgoing transaction locally', async () => {
|
|
|
802
803
|
})
|
|
803
804
|
})
|
|
804
805
|
|
|
805
|
-
const documentId = DocumentId(
|
|
806
|
+
const documentId = DocumentId(randomUuid())
|
|
806
807
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
807
808
|
|
|
808
809
|
const {getCurrent, subscribe} = getDocumentState(instance, doc)
|
|
@@ -870,7 +871,7 @@ it('removes a queued transaction if it fails to apply', async () => {
|
|
|
870
871
|
})
|
|
871
872
|
})
|
|
872
873
|
|
|
873
|
-
const doc = createDocumentHandle({documentId:
|
|
874
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
874
875
|
const state = getDocumentState(instance, doc)
|
|
875
876
|
const unsubscribe = state.subscribe()
|
|
876
877
|
|
|
@@ -975,8 +976,8 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
975
976
|
]
|
|
976
977
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
977
978
|
|
|
978
|
-
const book = createDocumentHandle({documentId:
|
|
979
|
-
const author = createDocumentHandle({documentId:
|
|
979
|
+
const book = createDocumentHandle({documentId: randomUuid(), documentType: 'book'})
|
|
980
|
+
const author = createDocumentHandle({documentId: randomUuid(), documentType: 'author'})
|
|
980
981
|
|
|
981
982
|
expect(await resolvePermissions(instance, {actions: [createDocument(book)]})).toEqual({
|
|
982
983
|
allowed: true,
|
|
@@ -988,12 +989,12 @@ it('fetches dataset ACL and updates grants in the document store state', async (
|
|
|
988
989
|
})
|
|
989
990
|
|
|
990
991
|
it('does not send credentials with the dataset ACL request', async () => {
|
|
991
|
-
const doc = createDocumentHandle({documentId:
|
|
992
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
992
993
|
await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
993
994
|
|
|
994
995
|
const aclCall = vi
|
|
995
996
|
.mocked(client.request)
|
|
996
|
-
.mock.calls.find(([options]) => options.
|
|
997
|
+
.mock.calls.find(([options]) => options.url?.endsWith('/acl'))
|
|
997
998
|
expect(aclCall).toBeDefined()
|
|
998
999
|
expect(aclCall![0]).not.toHaveProperty('withCredentials')
|
|
999
1000
|
})
|
|
@@ -1011,7 +1012,7 @@ it('retries transient dataset ACL fetch failures instead of failing fatally', as
|
|
|
1011
1012
|
}),
|
|
1012
1013
|
)
|
|
1013
1014
|
|
|
1014
|
-
const doc = createDocumentHandle({documentId:
|
|
1015
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1015
1016
|
const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
1016
1017
|
|
|
1017
1018
|
expect(result).toEqual({allowed: true})
|
|
@@ -1037,7 +1038,7 @@ it('does not retry dataset ACL fetch failures caused by 4xx client errors', asyn
|
|
|
1037
1038
|
}),
|
|
1038
1039
|
)
|
|
1039
1040
|
|
|
1040
|
-
const doc = createDocumentHandle({documentId:
|
|
1041
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1041
1042
|
const documentState = getDocumentState(instance, doc)
|
|
1042
1043
|
const unsubscribe = documentState.subscribe()
|
|
1043
1044
|
|
|
@@ -1070,7 +1071,7 @@ it('retries dataset ACL fetch failures caused by 429 rate-limit errors', async (
|
|
|
1070
1071
|
}),
|
|
1071
1072
|
)
|
|
1072
1073
|
|
|
1073
|
-
const doc = createDocumentHandle({documentId:
|
|
1074
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1074
1075
|
const result = await resolvePermissions(instance, {actions: [createDocument(doc)]})
|
|
1075
1076
|
|
|
1076
1077
|
expect(result).toEqual({allowed: true})
|
|
@@ -1087,7 +1088,7 @@ it('fetches ACL for MediaLibraryResource', async () => {
|
|
|
1087
1088
|
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
1088
1089
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
1089
1090
|
|
|
1090
|
-
const doc = createDocumentHandle({documentId:
|
|
1091
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1091
1092
|
const mediaLibraryResource = {mediaLibraryId: 'test-media-library'}
|
|
1092
1093
|
|
|
1093
1094
|
const result = await resolvePermissions(mediaLibraryInstance, {
|
|
@@ -1105,7 +1106,7 @@ it('fetches ACL for CanvasResource', async () => {
|
|
|
1105
1106
|
const datasetAcl = [{filter: 'true', permissions: ['read', 'update', 'create', 'history']}]
|
|
1106
1107
|
vi.mocked(client.request).mockResolvedValue(datasetAcl)
|
|
1107
1108
|
|
|
1108
|
-
const doc = createDocumentHandle({documentId:
|
|
1109
|
+
const doc = createDocumentHandle({documentId: randomUuid(), documentType: 'article'})
|
|
1109
1110
|
const canvasResource = {canvasId: 'test-canvas'}
|
|
1110
1111
|
|
|
1111
1112
|
const result = await resolvePermissions(canvasInstance, {
|
|
@@ -1118,7 +1119,7 @@ it('fetches ACL for CanvasResource', async () => {
|
|
|
1118
1119
|
})
|
|
1119
1120
|
|
|
1120
1121
|
it('returns a promise that resolves when a document has been loaded in the store (useful for suspense)', async () => {
|
|
1121
|
-
const documentId = DocumentId(
|
|
1122
|
+
const documentId = DocumentId(randomUuid())
|
|
1122
1123
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
1123
1124
|
|
|
1124
1125
|
expect(await resolveDocument(instance, doc)).toBe(null)
|
|
@@ -1143,7 +1144,7 @@ it('emits an event for each action after an outgoing transaction has been accept
|
|
|
1143
1144
|
const handler = vi.fn()
|
|
1144
1145
|
const unsubscribe = subscribeDocumentEvents(instance, {resource, eventHandler: handler})
|
|
1145
1146
|
|
|
1146
|
-
const documentId = DocumentId(
|
|
1147
|
+
const documentId = DocumentId(randomUuid())
|
|
1147
1148
|
const doc = createDocumentHandle({documentId, documentType: 'article'})
|
|
1148
1149
|
expect(handler).toHaveBeenCalledTimes(0)
|
|
1149
1150
|
|
|
@@ -1574,7 +1575,7 @@ beforeEach(() => {
|
|
|
1574
1575
|
mutations: Mutation[],
|
|
1575
1576
|
options: BaseMutationOptions = {},
|
|
1576
1577
|
): Promise<MultipleMutationResult> => {
|
|
1577
|
-
const transactionId = options.transactionId ??
|
|
1578
|
+
const transactionId = options.transactionId ?? randomUuid()
|
|
1578
1579
|
const timestamp = new Date().toISOString()
|
|
1579
1580
|
const prior = {...documents}
|
|
1580
1581
|
let next = {...documents}
|
|
@@ -1647,7 +1648,7 @@ beforeEach(() => {
|
|
|
1647
1648
|
const action = vi.fn(
|
|
1648
1649
|
async (
|
|
1649
1650
|
input: HttpAction | HttpAction[],
|
|
1650
|
-
{transactionId =
|
|
1651
|
+
{transactionId = randomUuid(), dryRun}: BaseActionOptions,
|
|
1651
1652
|
): Promise<SingleActionResult | MultipleActionResult> => {
|
|
1652
1653
|
const actions = Array.isArray(input) ? input : [input]
|
|
1653
1654
|
let next: DocumentSet = {...documents}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {type Action, ClientError, CorsOriginError, type Mutation} from '@sanity/client'
|
|
2
2
|
import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
3
3
|
import {jsonMatch} from '@sanity/json-match'
|
|
4
|
-
import {type SanityDocument} from 'groq'
|
|
5
4
|
import {type ExprNode} from 'groq-js'
|
|
6
5
|
import {
|
|
7
6
|
catchError,
|
|
@@ -46,6 +45,7 @@ import {
|
|
|
46
45
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
47
46
|
import {createStateSourceAction, type StateSource} from '../store/createStateSourceAction'
|
|
48
47
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
48
|
+
import {type ResolveDocument} from '../typegen/resolve'
|
|
49
49
|
import {type DocumentAction} from './actions'
|
|
50
50
|
import {
|
|
51
51
|
ACL_RETRY_BASE_DELAY,
|
|
@@ -98,7 +98,7 @@ export interface DocumentStoreState {
|
|
|
98
98
|
identity?: string
|
|
99
99
|
error?: unknown
|
|
100
100
|
sharedListener: SharedListener
|
|
101
|
-
fetchDocument: (documentId: string) => Observable<
|
|
101
|
+
fetchDocument: (documentId: string) => Observable<ResolveDocument | null>
|
|
102
102
|
events: Subject<DocumentEvent>
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -108,12 +108,12 @@ export interface DocumentState {
|
|
|
108
108
|
* the "remote" local copy that matches the server. represents the last known
|
|
109
109
|
* server state. this gets updated every time we confirm remote patches
|
|
110
110
|
*/
|
|
111
|
-
remote?:
|
|
111
|
+
remote?: ResolveDocument | null
|
|
112
112
|
/**
|
|
113
113
|
* the current ephemeral working copy that includes local optimistic changes
|
|
114
114
|
* that have not yet been confirmed by the server
|
|
115
115
|
*/
|
|
116
|
-
local?:
|
|
116
|
+
local?: ResolveDocument | null
|
|
117
117
|
/**
|
|
118
118
|
* the revision that our remote document is at
|
|
119
119
|
*/
|
|
@@ -194,7 +194,7 @@ export function getDocumentState<
|
|
|
194
194
|
>(
|
|
195
195
|
instance: SanityInstance,
|
|
196
196
|
options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
197
|
-
): StateSource<
|
|
197
|
+
): StateSource<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`> | undefined | null>
|
|
198
198
|
|
|
199
199
|
/** @beta */
|
|
200
200
|
export function getDocumentState<
|
|
@@ -206,7 +206,7 @@ export function getDocumentState<
|
|
|
206
206
|
instance: SanityInstance,
|
|
207
207
|
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
|
|
208
208
|
): StateSource<
|
|
209
|
-
JsonMatch<
|
|
209
|
+
JsonMatch<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
|
|
210
210
|
>
|
|
211
211
|
|
|
212
212
|
/** @beta */
|
|
@@ -229,12 +229,12 @@ const _getDocumentState = bindActionByResource(
|
|
|
229
229
|
const {documentId: docId, path, liveEdit, perspective} = options
|
|
230
230
|
const documentId = DocumentId(docId)
|
|
231
231
|
if (error) throw error
|
|
232
|
-
let document:
|
|
232
|
+
let document: ResolveDocument | null | undefined
|
|
233
233
|
|
|
234
234
|
if (liveEdit) {
|
|
235
235
|
document = documentStates[documentId]?.local
|
|
236
236
|
} else {
|
|
237
|
-
let version:
|
|
237
|
+
let version: ResolveDocument | null | undefined
|
|
238
238
|
if (isReleasePerspective(perspective)) {
|
|
239
239
|
const versionId = getVersionId(documentId, perspective.releaseName)
|
|
240
240
|
version = documentStates[versionId]?.local
|
|
@@ -267,16 +267,16 @@ export function resolveDocument<
|
|
|
267
267
|
>(
|
|
268
268
|
instance: SanityInstance,
|
|
269
269
|
docHandle: DocumentHandle<TDocumentType, TDataset, TProjectId>,
|
|
270
|
-
): Promise<
|
|
270
|
+
): Promise<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null>
|
|
271
271
|
/** @beta */
|
|
272
|
-
export function resolveDocument<TData extends
|
|
272
|
+
export function resolveDocument<TData extends ResolveDocument>(
|
|
273
273
|
instance: SanityInstance,
|
|
274
274
|
docHandle: DocumentHandle<string, string, string>,
|
|
275
275
|
): Promise<TData | null>
|
|
276
276
|
/** @beta */
|
|
277
277
|
export function resolveDocument(
|
|
278
278
|
...args: Parameters<typeof _resolveDocument>
|
|
279
|
-
): Promise<
|
|
279
|
+
): Promise<ResolveDocument | null> {
|
|
280
280
|
return _resolveDocument(...args)
|
|
281
281
|
}
|
|
282
282
|
const _resolveDocument = bindActionByResource(
|
|
@@ -287,7 +287,7 @@ const _resolveDocument = bindActionByResource(
|
|
|
287
287
|
...docHandle,
|
|
288
288
|
path: undefined,
|
|
289
289
|
}).observable.pipe(filter((i) => i !== undefined)),
|
|
290
|
-
) as Promise<
|
|
290
|
+
) as Promise<ResolveDocument | null>
|
|
291
291
|
},
|
|
292
292
|
)
|
|
293
293
|
|
|
@@ -550,13 +550,13 @@ const subscribeToClientAndFetchDatasetAcl = ({
|
|
|
550
550
|
}: StoreContext<DocumentStoreState, BoundResourceKey>) => {
|
|
551
551
|
const clientOptions: ClientOptions = {apiVersion: API_VERSION, resource}
|
|
552
552
|
|
|
553
|
-
let
|
|
553
|
+
let url: string
|
|
554
554
|
if (resource && isDatasetResource(resource)) {
|
|
555
|
-
|
|
555
|
+
url = `/projects/${resource.projectId}/datasets/${resource.dataset}/acl`
|
|
556
556
|
} else if (resource && isMediaLibraryResource(resource)) {
|
|
557
|
-
|
|
557
|
+
url = `/media-libraries/${resource.mediaLibraryId}/acl`
|
|
558
558
|
} else if (resource && isCanvasResource(resource)) {
|
|
559
|
-
|
|
559
|
+
url = `/canvases/${resource.canvasId}/acl`
|
|
560
560
|
} else {
|
|
561
561
|
throw new Error(`Received invalid resource: ${JSON.stringify(resource)}`)
|
|
562
562
|
}
|
|
@@ -566,7 +566,7 @@ const subscribeToClientAndFetchDatasetAcl = ({
|
|
|
566
566
|
switchMap((client) =>
|
|
567
567
|
client.observable
|
|
568
568
|
.request<DatasetAcl>({
|
|
569
|
-
|
|
569
|
+
url,
|
|
570
570
|
tag: 'acl.get',
|
|
571
571
|
})
|
|
572
572
|
.pipe(
|
|
@@ -5,6 +5,7 @@ import {createSelector} from 'reselect'
|
|
|
5
5
|
|
|
6
6
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
7
7
|
import {type SelectorContext} from '../store/createStateSourceAction'
|
|
8
|
+
import {randomId} from '../utils/ids'
|
|
8
9
|
import {MultiKeyWeakMap} from '../utils/MultiKeyWeakMap'
|
|
9
10
|
import {type DocumentAction} from './actions'
|
|
10
11
|
import {ActionError, PermissionActionError, processActions} from './processActions/processActions'
|
|
@@ -190,7 +191,7 @@ const _calculatePermissions = createSelector(
|
|
|
190
191
|
try {
|
|
191
192
|
processActions({
|
|
192
193
|
actions,
|
|
193
|
-
transactionId:
|
|
194
|
+
transactionId: randomId(16),
|
|
194
195
|
working: documents,
|
|
195
196
|
base: documents,
|
|
196
197
|
timestamp,
|
|
@@ -2,6 +2,7 @@ import {type CreateMutation, type Reference, type SanityDocument} from '@sanity/
|
|
|
2
2
|
import {parse} from 'groq-js'
|
|
3
3
|
import {describe, expect, it} from 'vitest'
|
|
4
4
|
|
|
5
|
+
import {randomUuid} from '../utils/ids'
|
|
5
6
|
import {type Action, type DocumentAction} from './actions'
|
|
6
7
|
import {ActionError, processActions} from './processActions/processActions'
|
|
7
8
|
import {type DocumentSet, processMutations} from './processMutations'
|
|
@@ -973,7 +974,7 @@ describe('processActions', () => {
|
|
|
973
974
|
})
|
|
974
975
|
|
|
975
976
|
it('should strengthen `_strengthenOnPublish` references', () => {
|
|
976
|
-
const _ref =
|
|
977
|
+
const _ref = randomUuid()
|
|
977
978
|
const referenceToStrength: Reference = {
|
|
978
979
|
_ref,
|
|
979
980
|
_type: 'reference',
|