From d4695ae9c7c4561ccc5f7c5c734653ea688b2aba Mon Sep 17 00:00:00 2001 From: Carolina Gonzalez Date: Tue, 16 Sep 2025 09:49:25 -0400 Subject: [PATCH] fix(core): change ValidProjection to string type --- .../DocumentCollection/MultiResourceRoute.tsx | 2 +- .../src/routes/releases/ReleasesRoute.tsx | 2 +- .../core/src/projection/getProjectionState.ts | 10 ++++----- .../src/projection/projectionQuery.test.ts | 11 +++++----- .../core/src/projection/projectionQuery.ts | 11 ++++------ .../src/projection/resolveProjection.test.ts | 4 ++-- .../core/src/projection/resolveProjection.ts | 4 ++-- packages/core/src/projection/types.ts | 15 ++++++++----- packages/core/src/projection/util.ts | 6 ++--- .../projection/useDocumentProjection.test.tsx | 22 ++++--------------- .../hooks/projection/useDocumentProjection.ts | 11 +++------- 11 files changed, 38 insertions(+), 60 deletions(-) diff --git a/apps/kitchensink-react/src/DocumentCollection/MultiResourceRoute.tsx b/apps/kitchensink-react/src/DocumentCollection/MultiResourceRoute.tsx index fc4a4c34e..175e8b1ed 100644 --- a/apps/kitchensink-react/src/DocumentCollection/MultiResourceRoute.tsx +++ b/apps/kitchensink-react/src/DocumentCollection/MultiResourceRoute.tsx @@ -70,7 +70,7 @@ function ProjectionCard({ const {data, isPending} = useDocumentProjection({ ...docHandle, ref, - projection: projection as `{${string}}`, + projection: projection, }) return ( diff --git a/apps/kitchensink-react/src/routes/releases/ReleasesRoute.tsx b/apps/kitchensink-react/src/routes/releases/ReleasesRoute.tsx index 4b3d09af7..fad2f6584 100644 --- a/apps/kitchensink-react/src/routes/releases/ReleasesRoute.tsx +++ b/apps/kitchensink-react/src/routes/releases/ReleasesRoute.tsx @@ -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], ) diff --git a/packages/core/src/projection/getProjectionState.ts b/packages/core/src/projection/getProjectionState.ts index 578e180e4..da6fb169b 100644 --- a/packages/core/src/projection/getProjectionState.ts +++ b/packages/core/src/projection/getProjectionState.ts @@ -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, @@ -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, @@ -75,13 +75,13 @@ export const _getProjectionState = bindActionByDataset( createStateSourceAction({ selector: ( {state}: SelectorContext, - options: ProjectionOptions, + options: ProjectionOptions, ): ProjectionValuePending | undefined => { const documentId = getPublishedId(options.documentId) const projectionHash = hashString(options.projection) return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION }, - onSubscribe: ({state}, options: ProjectionOptions) => { + onSubscribe: ({state}, options: ProjectionOptions) => { const {projection, ...docHandle} = options const subscriptionId = insecureRandomId() const documentId = getPublishedId(docHandle.documentId) diff --git a/packages/core/src/projection/projectionQuery.test.ts b/packages/core/src/projection/projectionQuery.test.ts index a1d925709..178511e0e 100644 --- a/packages/core/src/projection/projectionQuery.test.ts +++ b/packages/core/src/projection/projectionQuery.test.ts @@ -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}, @@ -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}, @@ -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}, diff --git a/packages/core/src/projection/projectionQuery.ts b/packages/core/src/projection/projectionQuery.ts index 8607050ed..fc92b88aa 100644 --- a/packages/core/src/projection/projectionQuery.ts +++ b/packages/core/src/projection/projectionQuery.ts @@ -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 @@ -18,7 +15,7 @@ interface CreateProjectionQueryResult { params: Record } -type ProjectionMap = Record}> +type ProjectionMap = Record}> export function createProjectionQuery( documentIds: Set, @@ -31,7 +28,7 @@ export function createProjectionQuery( return Object.entries(projectionsForDoc).map(([projectionHash, projection]) => ({ documentId: id, - projection, + projection: validateProjection(projection), projectionHash, })) }) diff --git a/packages/core/src/projection/resolveProjection.test.ts b/packages/core/src/projection/resolveProjection.test.ts index 441ff852c..99394a3c6 100644 --- a/packages/core/src/projection/resolveProjection.test.ts +++ b/packages/core/src/projection/resolveProjection.test.ts @@ -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') @@ -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}) diff --git a/packages/core/src/projection/resolveProjection.ts b/packages/core/src/projection/resolveProjection.ts index 092d7170f..8cc92f097 100644 --- a/packages/core/src/projection/resolveProjection.ts +++ b/packages/core/src/projection/resolveProjection.ts @@ -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, diff --git a/packages/core/src/projection/types.ts b/packages/core/src/projection/types.ts index 41b9ee030..4810ef270 100644 --- a/packages/core/src/projection/types.ts +++ b/packages/core/src/projection/types.ts @@ -1,8 +1,3 @@ -/** - * @public - */ -export type ValidProjection = `{${string}}` - /** * @public * The result of a projection query @@ -16,8 +11,16 @@ export interface DocumentProjectionValues { [projectionHash: string]: ProjectionValuePending } +/** + * @public + * @deprecated + * 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 { diff --git a/packages/core/src/projection/util.ts b/packages/core/src/projection/util.ts index ec5d78cdd..2da45f178 100644 --- a/packages/core/src/projection/util.ts +++ b/packages/core/src/projection/util.ts @@ -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 @@ -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 } diff --git a/packages/react/src/hooks/projection/useDocumentProjection.test.tsx b/packages/react/src/hooks/projection/useDocumentProjection.test.tsx index f25c652d6..0e529f19a 100644 --- a/packages/react/src/hooks/projection/useDocumentProjection.test.tsx +++ b/packages/react/src/hooks/projection/useDocumentProjection.test.tsx @@ -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' @@ -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({...document, projection, ref}) @@ -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({...docHandle, projection}) // No ref provided return (
@@ -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({...docHandle, projection, ref}) return ( diff --git a/packages/react/src/hooks/projection/useDocumentProjection.ts b/packages/react/src/hooks/projection/useDocumentProjection.ts index 1d2b67951..fe8a08457 100644 --- a/packages/react/src/hooks/projection/useDocumentProjection.ts +++ b/packages/react/src/hooks/projection/useDocumentProjection.ts @@ -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' @@ -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, @@ -115,7 +110,7 @@ export interface useDocumentProjectionResults { * ``` */ export function useDocumentProjection< - TProjection extends ValidProjection = ValidProjection, + TProjection extends string = string, TDocumentType extends string = string, TDataset extends string = string, TProjectId extends string = string,