Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function ProjectionCard<TData = unknown>({
const {data, isPending} = useDocumentProjection({
...docHandle,
ref,
projection: projection as `{${string}}`,
projection: projection,
})

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function ReleasesRoute(): JSX.Element {
const documentProjectionOptions = useMemo(
() => ({
...documentOptions,
projection: `{name, "bestFriend": bestFriend->name}` as `{${string}}`,
projection: `{name, "bestFriend": bestFriend->name}`,
}),
[documentOptions],
)
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/projection/getProjectionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
import {hashString} from '../utils/hashString'
import {getPublishedId, insecureRandomId} from '../utils/ids'
import {projectionStore} from './projectionStore'
import {type ProjectionStoreState, type ProjectionValuePending, type ValidProjection} from './types'
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'

export interface ProjectionOptions<
TProjection extends ValidProjection = ValidProjection,
TProjection extends string = string,
TDocumentType extends string = string,
TDataset extends string = string,
TProjectId extends string = string,
Expand All @@ -28,7 +28,7 @@ export interface ProjectionOptions<
* @beta
*/
export function getProjectionState<
TProjection extends ValidProjection = ValidProjection,
TProjection extends string = string,
TDocumentType extends string = string,
TDataset extends string = string,
TProjectId extends string = string,
Expand Down Expand Up @@ -75,13 +75,13 @@ export const _getProjectionState = bindActionByDataset(
createStateSourceAction({
selector: (
{state}: SelectorContext<ProjectionStoreState>,
options: ProjectionOptions<ValidProjection, string, string, string>,
options: ProjectionOptions<string, string, string, string>,
): ProjectionValuePending<object> | undefined => {
const documentId = getPublishedId(options.documentId)
const projectionHash = hashString(options.projection)
return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION
},
onSubscribe: ({state}, options: ProjectionOptions<ValidProjection, string, string, string>) => {
onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
const {projection, ...docHandle} = options
const subscriptionId = insecureRandomId()
const documentId = getPublishedId(docHandle.documentId)
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/projection/projectionQuery.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {describe, expect, it} from 'vitest'

import {createProjectionQuery, processProjectionQuery} from './projectionQuery'
import {type ValidProjection} from './types'

describe('createProjectionQuery', () => {
it('creates a query and params for given ids and projections', () => {
const ids = new Set(['doc1', 'doc2'])
const projectionHash: ValidProjection = '{title, description}'
const projectionHash = '{title, description}'
const documentProjections = {
doc1: {[projectionHash]: projectionHash},
doc2: {[projectionHash]: projectionHash},
Expand All @@ -21,8 +20,8 @@ describe('createProjectionQuery', () => {

it('handles multiple different projections', () => {
const ids = new Set(['doc1', 'doc2'])
const projectionHash1: ValidProjection = '{title, description}'
const projectionHash2: ValidProjection = '{name, age}'
const projectionHash1 = '{title, description}'
const projectionHash2 = '{name, age}'
const documentProjections = {
doc1: {[projectionHash1]: projectionHash1},
doc2: {[projectionHash2]: projectionHash2},
Expand All @@ -39,9 +38,9 @@ describe('createProjectionQuery', () => {

it('filters out ids without projections', () => {
const ids = new Set(['doc1', 'doc2', 'doc3'])
const projectionHash1: ValidProjection = '{title}'
const projectionHash1 = '{title}'
// projectionHash2 missing intentionally
const projectionHash3: ValidProjection = '{name}'
const projectionHash3 = '{name}'

const documentProjections = {
doc1: {[projectionHash1]: projectionHash1},
Expand Down
11 changes: 4 additions & 7 deletions packages/core/src/projection/projectionQuery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import {getDraftId, getPublishedId} from '../utils/ids'
import {
type DocumentProjections,
type DocumentProjectionValues,
type ValidProjection,
} from './types'
import {type DocumentProjections, type DocumentProjectionValues} from './types'
import {validateProjection} from './util'

export type ProjectionQueryResult = {
_id: string
Expand All @@ -18,7 +15,7 @@ interface CreateProjectionQueryResult {
params: Record<string, unknown>
}

type ProjectionMap = Record<string, {projection: ValidProjection; documentIds: Set<string>}>
type ProjectionMap = Record<string, {projection: string; documentIds: Set<string>}>

export function createProjectionQuery(
documentIds: Set<string>,
Expand All @@ -31,7 +28,7 @@ export function createProjectionQuery(

return Object.entries(projectionsForDoc).map(([projectionHash, projection]) => ({
documentId: id,
projection,
projection: validateProjection(projection),
projectionHash,
}))
})
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/projection/resolveProjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {createSanityInstance, type SanityInstance} from '../store/createSanityIn
import {type StateSource} from '../store/createStateSourceAction'
import {getProjectionState} from './getProjectionState'
import {resolveProjection} from './resolveProjection'
import {type ProjectionValuePending, type ValidProjection} from './types'
import {type ProjectionValuePending} from './types'

vi.mock('./getProjectionState')

Expand Down Expand Up @@ -35,7 +35,7 @@ describe('resolveProjection', () => {
documentId: 'doc123',
documentType: 'movie',
})
const projection = '{title}' as ValidProjection
const projection = '{title}'

const result = await resolveProjection(instance, {...docHandle, projection})

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/projection/resolveProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {bindActionByDataset} from '../store/createActionBinder'
import {type SanityInstance} from '../store/createSanityInstance'
import {getProjectionState, type ProjectionOptions} from './getProjectionState'
import {projectionStore} from './projectionStore'
import {type ProjectionValuePending, type ValidProjection} from './types'
import {type ProjectionValuePending} from './types'

/** @beta */
export function resolveProjection<
TProjection extends ValidProjection = ValidProjection,
TProjection extends string = string,
TDocumentType extends string = string,
TDataset extends string = string,
TProjectId extends string = string,
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/projection/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* @public
*/
export type ValidProjection = `{${string}}`

/**
* @public
* The result of a projection query
Expand All @@ -16,8 +11,16 @@ export interface DocumentProjectionValues<TValue extends object = object> {
[projectionHash: string]: ProjectionValuePending<TValue>
}

/**
* @public
* @deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(question) do we want to mark this as deprecated or do we want to mark the version that this goes out with as a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can certainly use it as a reason to go up a version but I can't think of a scenario in which this might break someone's builds. The type has become more permissive so anyone who used ValidProjection explicitly shouldn't get any type errors, I don't think (maybe some linting against deprecations?)...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, it likely would never cause any issues for users. I think I would rather remove it than mark it as deprecated in this case. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think removing an export would be a breaking change and require going up a version. We can do that too but maybe want to strategize on breaking changes first.

It would be good to add a todo list item to remove all deprecated types when we go up a version, certainly.

* Template literals are a bit too limited, so this type is deprecated.
* Use `string` instead. Projection strings are validated at runtime.
*/
export type ValidProjection = string

export interface DocumentProjections {
[projectionHash: string]: ValidProjection
[projectionHash: string]: string
}

interface DocumentProjectionSubscriptions {
Expand Down
6 changes: 2 additions & 4 deletions packages/core/src/projection/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {type ValidProjection} from './types'

export const PROJECTION_TAG = 'projection'
export const PROJECTION_PERSPECTIVE = 'raw'
export const PROJECTION_STATE_CLEAR_DELAY = 1000
Expand All @@ -9,11 +7,11 @@ export const STABLE_EMPTY_PROJECTION = {
isPending: false,
}

export function validateProjection(projection: string): ValidProjection {
export function validateProjection(projection: string): string {
if (!projection.startsWith('{') || !projection.endsWith('}')) {
throw new Error(
`Invalid projection format: "${projection}". Projections must be enclosed in curly braces, e.g. "{title, 'author': author.name}"`,
)
}
return projection as ValidProjection
return projection
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
type DocumentHandle,
getProjectionState,
resolveProjection,
type ValidProjection,
} from '@sanity/sdk'
import {type DocumentHandle, getProjectionState, resolveProjection} from '@sanity/sdk'
import {act, render, screen} from '@testing-library/react'
import {Suspense, useRef} from 'react'
import {type Mock} from 'vitest'
Expand Down Expand Up @@ -53,13 +48,7 @@ interface ProjectionResult {
description: string
}

function TestComponent({
document,
projection,
}: {
document: DocumentHandle
projection: ValidProjection
}) {
function TestComponent({document, projection}: {document: DocumentHandle; projection: string}) {
const ref = useRef(null)
const {data, isPending} = useDocumentProjection<ProjectionResult>({...document, projection, ref})

Expand Down Expand Up @@ -224,10 +213,7 @@ describe('useDocumentProjection', () => {
const eventsUnsubscribe = vi.fn()
subscribe.mockImplementation(() => eventsUnsubscribe)

function NoRefComponent({
projection,
...docHandle
}: DocumentHandle & {projection: ValidProjection}) {
function NoRefComponent({projection, ...docHandle}: DocumentHandle & {projection: string}) {
const {data} = useDocumentProjection<ProjectionResult>({...docHandle, projection}) // No ref provided
return (
<div>
Expand Down Expand Up @@ -259,7 +245,7 @@ describe('useDocumentProjection', () => {
function NonHtmlRefComponent({
projection,
...docHandle
}: DocumentHandle & {projection: ValidProjection}) {
}: DocumentHandle & {projection: string}) {
const ref = useRef({}) // ref.current is not an HTML element
const {data} = useDocumentProjection<ProjectionResult>({...docHandle, projection, ref})
return (
Expand Down
11 changes: 3 additions & 8 deletions packages/react/src/hooks/projection/useDocumentProjection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
type DocumentHandle,
getProjectionState,
resolveProjection,
type ValidProjection,
} from '@sanity/sdk'
import {type DocumentHandle, getProjectionState, resolveProjection} from '@sanity/sdk'
import {type SanityProjectionResult} from 'groq'
import {useCallback, useSyncExternalStore} from 'react'
import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
Expand All @@ -15,7 +10,7 @@ import {useSanityInstance} from '../context/useSanityInstance'
* @category Types
*/
export interface useDocumentProjectionOptions<
TProjection extends ValidProjection = ValidProjection,
TProjection extends string = string,
TDocumentType extends string = string,
TDataset extends string = string,
TProjectId extends string = string,
Expand Down Expand Up @@ -115,7 +110,7 @@ export interface useDocumentProjectionResults<TData> {
* ```
*/
export function useDocumentProjection<
TProjection extends ValidProjection = ValidProjection,
TProjection extends string = string,
TDocumentType extends string = string,
TDataset extends string = string,
TProjectId extends string = string,
Expand Down
Loading