sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
117 lines (102 loc) • 3.94 kB
text/typescript
import {isEqual} from 'lodash'
import {useEffect, useReducer, useRef} from 'react'
import {distinctUntilChanged, type Observable, type Subscription} from 'rxjs'
import {type LoadingTuple, type PartialExcept} from '../../../util'
import {useGrantsStore} from '../datastores'
import {type DocumentValuePermission, type GrantsStore, type PermissionCheckResult} from './types'
/** @internal */
export interface DocumentValuePermissionsOptions {
grantsStore: GrantsStore
document: Record<string, unknown>
permission: DocumentValuePermission
}
/**
* Gets permissions based on the value of the document passed into the hook
* (stateless).
*
* Note: this is a lower-level API (compared to `useDocumentPairPermissions`)
* that is _not_ draft-model aware.
*
* As a consequence, the operations it accepts are also low-level. (e.g.
* `'publish'` permissions can't be determined with this API). This is because
* it's not possible to tell if a user can do high-level document pair
* operations on document using only one document value.
*
* For example, in order to determine if a user can publish, the current value
* of the published document needs to be pulled and checked against the user's
* grants. If there are no matching grants, then it fails the pre-condition and
* no operation is allowed regardless of the given document.
*
* @see useDocumentPairPermissions
*
* @internal
*/
export function getDocumentValuePermissions({
grantsStore,
document,
permission,
}: DocumentValuePermissionsOptions): Observable<PermissionCheckResult> {
const {checkDocumentPermission} = grantsStore
return checkDocumentPermission(permission, document)
}
const INITIAL_STATE: LoadingTuple<PermissionCheckResult | undefined> = [undefined, true]
function stateReducer(
prev: LoadingTuple<PermissionCheckResult | undefined>,
action:
| {type: 'loading'}
| {type: 'value'; value: PermissionCheckResult}
| {type: 'error'; error: unknown},
): LoadingTuple<PermissionCheckResult | undefined> {
const [prevResult, prevIsLoading] = prev
switch (action.type) {
// Keep the old value around while loading a new one. Prevents "jittering" UIs, and permissions
// usually don't change _that_ rapidly (this hook runs on every single document change).
case 'loading':
return [prevResult, true]
case 'value':
// Signal "no update" to React if we've got the same value as before by returning old state
return !prevIsLoading && isEqual(action.value, prevResult) ? prev : [action.value, false]
case 'error':
throw action.error
default:
throw new Error(`Invalid action type: ${action}`)
}
}
/** @internal */
export function useDocumentValuePermissions({
document,
permission,
grantsStore: specifiedGrantsStore,
}: PartialExcept<DocumentValuePermissionsOptions, 'permission' | 'document'>): LoadingTuple<
PermissionCheckResult | undefined
> {
const defaultGrantsStore = useGrantsStore()
const grantsStore = specifiedGrantsStore || defaultGrantsStore
const [state, dispatch] = useReducer(stateReducer, INITIAL_STATE)
const subscriptionRef = useRef<Subscription | null>(null)
useEffect(() => {
dispatch({type: 'loading'})
// Unsubscribe from any previous subscription
if (subscriptionRef.current) {
subscriptionRef.current.unsubscribe()
}
const permissions$ = getDocumentValuePermissions({
grantsStore,
document,
permission,
})
subscriptionRef.current = permissions$
.pipe(distinctUntilChanged((prev, next) => isEqual(prev, next)))
.subscribe({
next: (value) => dispatch({type: 'value', value}),
error: (error) => dispatch({type: 'error', error}),
})
return () => {
if (subscriptionRef.current) {
subscriptionRef.current.unsubscribe()
subscriptionRef.current = null
}
}
}, [grantsStore, document, permission])
return state
}