Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/kitchensink-react/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {ProtectedRoute} from './ProtectedRoute'
import {DashboardContextRoute} from './routes/DashboardContextRoute'
import {DashboardWorkspacesRoute} from './routes/DashboardWorkspacesRoute'
import ExperimentalResourceClientRoute from './routes/ExperimentalResourceClientRoute'
import {MediaLibraryRoute} from './routes/MediaLibraryRoute'
import {PerspectivesRoute} from './routes/PerspectivesRoute'
import {ProjectsRoute} from './routes/ProjectsRoute'
import {ReleasesRoute} from './routes/releases/ReleasesRoute'
Expand Down Expand Up @@ -72,6 +73,10 @@ const documentCollectionRoutes = [
path: 'presence',
element: <PresenceRoute />,
},
{
path: 'media-library',
element: <MediaLibraryRoute />,
},
]

const dashboardInteractionRoutes = [
Expand Down
76 changes: 76 additions & 0 deletions apps/kitchensink-react/src/routes/MediaLibraryRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {mediaLibrarySource, useQuery} from '@sanity/sdk-react'
import {Card, Spinner, Text} from '@sanity/ui'
import {type JSX, useState} from 'react'

// for now, hardcoded. should be inferred from org later on
const MEDIA = mediaLibrarySource('mlPGY7BEqt52')

export function MediaLibraryRoute(): JSX.Element {
const [query] = useState('*[_type == "sanity.asset"][0...10] | order(_id desc)')
const [isLoading] = useState(false)

const {data, isPending} = useQuery({
query,
source: MEDIA,
})

return (
<div style={{padding: '2rem', maxWidth: '800px', margin: '0 auto'}}>
<Text size={4} weight="bold" style={{marginBottom: '2rem', color: 'white'}}>
Media Library Query Demo
</Text>

<Text size={2} style={{marginBottom: '2rem'}}>
This route demonstrates querying against a Sanity media library. The MediaLibraryProvider is
automatically created by SanityApp when a media library config is present. The query runs
against: <code>https://api.sanity.io/v2025-03-24/media-libraries/mlPGY7BEqt52/query</code>
</Text>

<Card padding={3} style={{marginBottom: '2rem', backgroundColor: '#1a1a1a'}}>
<div style={{marginBottom: '1rem'}}>
<Text size={1} style={{color: '#ccc', marginBottom: '0.5rem'}}>
Current query:
</Text>
<code
style={{
display: 'block',
padding: '0.5rem',
backgroundColor: '#2a2a2a',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '0.875rem',
color: '#fff',
wordBreak: 'break-all',
}}
>
{query}
</code>
</div>
</Card>

<Card padding={3} style={{backgroundColor: '#1a1a1a'}}>
<div style={{display: 'flex', alignItems: 'center', marginBottom: '1rem'}}>
<Text size={1} weight="medium" style={{color: '#fff'}}>
Query Results:
</Text>
{(isPending || isLoading) && <Spinner style={{marginLeft: '0.5rem'}} />}
</div>

<pre
style={{
backgroundColor: '#2a2a2a',
padding: '1rem',
borderRadius: '4px',
overflow: 'auto',
maxHeight: '400px',
fontSize: '0.875rem',
color: '#fff',
whiteSpace: 'pre-wrap',
}}
>
{JSON.stringify(data, null, 2)}
</pre>
</Card>
</div>
)
}
3 changes: 3 additions & 0 deletions packages/core/src/_exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ export {
} from '../config/handles'
export {
type DatasetHandle,
datasetSource,
type DocumentHandle,
type DocumentSource,
type DocumentTypeHandle,
mediaLibrarySource,
type PerspectiveHandle,
type ProjectHandle,
type ReleasePerspective,
Expand Down
20 changes: 18 additions & 2 deletions packages/core/src/client/clientStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {type ClientConfig, createClient, type SanityClient} from '@sanity/client
import {pick} from 'lodash-es'

import {getAuthMethodState, getTokenState} from '../auth/authStore'
import {type DocumentSource, SOURCE_ID} from '../config/sanityConfig'
import {bindActionGlobally} from '../store/createActionBinder'
import {createStateSourceAction} from '../store/createStateSourceAction'
import {defineStore, type StoreContext} from '../store/defineStore'
Expand Down Expand Up @@ -39,6 +40,7 @@ const allowedKeys = Object.keys({
'requestTagPrefix': null,
'useProjectHostname': null,
'~experimental_resource': null,
'source': null,
} satisfies Record<keyof ClientOptions, null>) as (keyof ClientOptions)[]

const DEFAULT_CLIENT_CONFIG: ClientConfig = {
Expand Down Expand Up @@ -90,6 +92,11 @@ export interface ClientOptions extends Pick<ClientConfig, AllowedClientConfigKey
* @internal
*/
'~experimental_resource'?: ClientConfig['~experimental_resource']

/**
* @internal
*/
'source'?: DocumentSource
}

const clientStore = defineStore<ClientStoreState>({
Expand Down Expand Up @@ -156,8 +163,16 @@ export const getClient = bindActionGlobally(

const tokenFromState = state.get().token
const {clients, authMethod} = state.get()
const projectId = options.projectId ?? instance.config.projectId
const dataset = options.dataset ?? instance.config.dataset
let sourceId = options.source?.[SOURCE_ID]

let resource
if (Array.isArray(sourceId)) {
resource = {type: sourceId[0], id: sourceId[1]}
sourceId = undefined
}

const projectId = options.projectId ?? instance.config.projectId ?? sourceId?.projectId
const dataset = options.dataset ?? instance.config.dataset ?? sourceId?.dataset
const apiHost = options.apiHost ?? instance.config.auth?.apiHost

const effectiveOptions: ClientOptions = {
Expand All @@ -168,6 +183,7 @@ export const getClient = bindActionGlobally(
...(projectId && {projectId}),
...(dataset && {dataset}),
...(apiHost && {apiHost}),
...(resource && {'~experimental_resource': resource}),
}

if (effectiveOptions.token === null || typeof effectiveOptions.token === 'undefined') {
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/config/sanityConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,34 @@ export interface SanityConfig extends DatasetHandle, PerspectiveHandle {
enabled: boolean
}
}

export const SOURCE_ID = '__sanity_internal_sourceId'

/**
* A document source can be used for querying.
*
* @beta
* @see datasetSource Construct a document source for a given projectId and dataset.
* @see mediaLibrarySource Construct a document source for a mediaLibraryId.
*/
export type DocumentSource = {
[SOURCE_ID]: ['media-library', string] | {projectId: string; dataset: string}
}

/**
* Returns a document source for a projectId and dataset.
*
* @beta
*/
export function datasetSource(projectId: string, dataset: string): DocumentSource {
return {[SOURCE_ID]: {projectId, dataset}}
}

/**
* Returns a document source for a Media Library.
*
* @beta
*/
export function mediaLibrarySource(id: string): DocumentSource {
return {[SOURCE_ID]: ['media-library', id]}
}
11 changes: 7 additions & 4 deletions packages/core/src/query/queryStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import {
} from 'rxjs'

import {getClientState} from '../client/clientStore'
import {type DatasetHandle} from '../config/sanityConfig'
import {type DatasetHandle, type DocumentSource} from '../config/sanityConfig'
import {getPerspectiveState} from '../releases/getPerspectiveState'
import {bindActionByDataset} from '../store/createActionBinder'
import {bindActionBySource} from '../store/createActionBinder'
import {type SanityInstance} from '../store/createSanityInstance'
import {
createStateSourceAction,
Expand Down Expand Up @@ -62,6 +62,7 @@ export interface QueryOptions<
DatasetHandle<TDataset, TProjectId> {
query: TQuery
params?: Record<string, unknown>
source?: DocumentSource
}

/**
Expand Down Expand Up @@ -160,6 +161,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
projectId,
dataset,
tag,
source,
perspective: perspectiveFromOptions,
...restOptions
} = parseQueryKey(group$.key)
Expand All @@ -172,6 +174,7 @@ const listenForNewSubscribersAndFetch = ({state, instance}: StoreContext<QuerySt
apiVersion: QUERY_STORE_API_VERSION,
projectId,
dataset,
source,
}).observable

return combineLatest([lastLiveEventId$, client$, perspective$]).pipe(
Expand Down Expand Up @@ -290,7 +293,7 @@ export function getQueryState(
): ReturnType<typeof _getQueryState> {
return _getQueryState(...args)
}
const _getQueryState = bindActionByDataset(
const _getQueryState = bindActionBySource(
queryStore,
createStateSourceAction({
selector: ({state, instance}: SelectorContext<QueryStoreState>, options: QueryOptions) => {
Expand Down Expand Up @@ -349,7 +352,7 @@ export function resolveQuery<TData>(
export function resolveQuery(...args: Parameters<typeof _resolveQuery>): Promise<unknown> {
return _resolveQuery(...args)
}
const _resolveQuery = bindActionByDataset(
const _resolveQuery = bindActionBySource(
queryStore,
({state, instance}, {signal, ...options}: ResolveQueryOptions) => {
const normalized = normalizeOptionsWithPerspective(instance, options)
Expand Down
33 changes: 27 additions & 6 deletions packages/core/src/store/createActionBinder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type SanityConfig} from '../config/sanityConfig'
import {type DocumentSource, type SanityConfig, SOURCE_ID} from '../config/sanityConfig'
import {type SanityInstance} from './createSanityInstance'
import {createStoreInstance, type StoreInstance} from './createStoreInstance'
import {type StoreState} from './createStoreState'
Expand Down Expand Up @@ -43,7 +43,9 @@ export type BoundStoreAction<_TState, TParams extends unknown[], TReturn> = (
* )
* ```
*/
export function createActionBinder(keyFn: (config: SanityConfig) => string) {
export function createActionBinder<TKeyParams extends unknown[]>(
keyFn: (config: SanityConfig, ...params: TKeyParams) => string,
) {
const instanceRegistry = new Map<string, Set<string>>()
const storeRegistry = new Map<string, StoreInstance<unknown>>()

Expand All @@ -54,12 +56,12 @@ export function createActionBinder(keyFn: (config: SanityConfig) => string) {
* @param action - The action to bind
* @returns A function that executes the action with a Sanity instance
*/
return function bindAction<TState, TParams extends unknown[], TReturn>(
return function bindAction<TState, TParams extends TKeyParams, TReturn>(
storeDefinition: StoreDefinition<TState>,
action: StoreAction<TState, TParams, TReturn>,
): BoundStoreAction<TState, TParams, TReturn> {
return function boundAction(instance: SanityInstance, ...params: TParams) {
const keySuffix = keyFn(instance.config)
const keySuffix = keyFn(instance.config, ...params)
const compositeKey = storeDefinition.name + (keySuffix ? `:${keySuffix}` : '')

// Get or create instance set for this composite key
Expand Down Expand Up @@ -128,13 +130,32 @@ export function createActionBinder(keyFn: (config: SanityConfig) => string) {
* fetchDocument(sanityInstance, 'doc123')
* ```
*/
export const bindActionByDataset = createActionBinder(({projectId, dataset}) => {
export const bindActionByDataset = createActionBinder<unknown[]>(({projectId, dataset}) => {
if (!projectId || !dataset) {
throw new Error('This API requires a project ID and dataset configured.')
}
return `${projectId}.${dataset}`
})

/**
* Binds an action to a store that's scoped to a specific document source.
**/
export const bindActionBySource = createActionBinder<[{source?: DocumentSource}, ...unknown[]]>(
({projectId, dataset}, {source}) => {
if (source) {
const id = source[SOURCE_ID]
if (!id) throw new Error('Invalid source (missing ID information)')
if (Array.isArray(id)) return id.join(':')
return `${id.projectId}.${id.dataset}`
}

if (!projectId || !dataset) {
throw new Error('This API requires a project ID and dataset configured.')
}
return `${projectId}.${dataset}`
},
)

/**
* Binds an action to a global store that's shared across all Sanity instances
*
Expand Down Expand Up @@ -173,4 +194,4 @@ export const bindActionByDataset = createActionBinder(({projectId, dataset}) =>
* getCurrentUser(sanityInstance)
* ```
*/
export const bindActionGlobally = createActionBinder(() => 'global')
export const bindActionGlobally = createActionBinder<unknown[]>(() => 'global')
Loading