Skip to content

Commit 2079b36

Browse files
authored
fix(core): change ValidProjection to string type (#627)
1 parent 7610ec2 commit 2079b36

File tree

11 files changed

+38
-60
lines changed

11 files changed

+38
-60
lines changed

apps/kitchensink-react/src/DocumentCollection/MultiResourceRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function ProjectionCard<TData = unknown>({
7070
const {data, isPending} = useDocumentProjection({
7171
...docHandle,
7272
ref,
73-
projection: projection as `{${string}}`,
73+
projection: projection,
7474
})
7575

7676
return (

apps/kitchensink-react/src/routes/releases/ReleasesRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function ReleasesRoute(): JSX.Element {
117117
const documentProjectionOptions = useMemo(
118118
() => ({
119119
...documentOptions,
120-
projection: `{name, "bestFriend": bestFriend->name}` as `{${string}}`,
120+
projection: `{name, "bestFriend": bestFriend->name}`,
121121
}),
122122
[documentOptions],
123123
)

packages/core/src/projection/getProjectionState.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import {
1212
import {hashString} from '../utils/hashString'
1313
import {getPublishedId, insecureRandomId} from '../utils/ids'
1414
import {projectionStore} from './projectionStore'
15-
import {type ProjectionStoreState, type ProjectionValuePending, type ValidProjection} from './types'
15+
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
1616
import {PROJECTION_STATE_CLEAR_DELAY, STABLE_EMPTY_PROJECTION, validateProjection} from './util'
1717

1818
export interface ProjectionOptions<
19-
TProjection extends ValidProjection = ValidProjection,
19+
TProjection extends string = string,
2020
TDocumentType extends string = string,
2121
TDataset extends string = string,
2222
TProjectId extends string = string,
@@ -28,7 +28,7 @@ export interface ProjectionOptions<
2828
* @beta
2929
*/
3030
export function getProjectionState<
31-
TProjection extends ValidProjection = ValidProjection,
31+
TProjection extends string = string,
3232
TDocumentType extends string = string,
3333
TDataset extends string = string,
3434
TProjectId extends string = string,
@@ -75,13 +75,13 @@ export const _getProjectionState = bindActionByDataset(
7575
createStateSourceAction({
7676
selector: (
7777
{state}: SelectorContext<ProjectionStoreState>,
78-
options: ProjectionOptions<ValidProjection, string, string, string>,
78+
options: ProjectionOptions<string, string, string, string>,
7979
): ProjectionValuePending<object> | undefined => {
8080
const documentId = getPublishedId(options.documentId)
8181
const projectionHash = hashString(options.projection)
8282
return state.values[documentId]?.[projectionHash] ?? STABLE_EMPTY_PROJECTION
8383
},
84-
onSubscribe: ({state}, options: ProjectionOptions<ValidProjection, string, string, string>) => {
84+
onSubscribe: ({state}, options: ProjectionOptions<string, string, string, string>) => {
8585
const {projection, ...docHandle} = options
8686
const subscriptionId = insecureRandomId()
8787
const documentId = getPublishedId(docHandle.documentId)

packages/core/src/projection/projectionQuery.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import {describe, expect, it} from 'vitest'
22

33
import {createProjectionQuery, processProjectionQuery} from './projectionQuery'
4-
import {type ValidProjection} from './types'
54

65
describe('createProjectionQuery', () => {
76
it('creates a query and params for given ids and projections', () => {
87
const ids = new Set(['doc1', 'doc2'])
9-
const projectionHash: ValidProjection = '{title, description}'
8+
const projectionHash = '{title, description}'
109
const documentProjections = {
1110
doc1: {[projectionHash]: projectionHash},
1211
doc2: {[projectionHash]: projectionHash},
@@ -21,8 +20,8 @@ describe('createProjectionQuery', () => {
2120

2221
it('handles multiple different projections', () => {
2322
const ids = new Set(['doc1', 'doc2'])
24-
const projectionHash1: ValidProjection = '{title, description}'
25-
const projectionHash2: ValidProjection = '{name, age}'
23+
const projectionHash1 = '{title, description}'
24+
const projectionHash2 = '{name, age}'
2625
const documentProjections = {
2726
doc1: {[projectionHash1]: projectionHash1},
2827
doc2: {[projectionHash2]: projectionHash2},
@@ -39,9 +38,9 @@ describe('createProjectionQuery', () => {
3938

4039
it('filters out ids without projections', () => {
4140
const ids = new Set(['doc1', 'doc2', 'doc3'])
42-
const projectionHash1: ValidProjection = '{title}'
41+
const projectionHash1 = '{title}'
4342
// projectionHash2 missing intentionally
44-
const projectionHash3: ValidProjection = '{name}'
43+
const projectionHash3 = '{name}'
4544

4645
const documentProjections = {
4746
doc1: {[projectionHash1]: projectionHash1},

packages/core/src/projection/projectionQuery.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import {getDraftId, getPublishedId} from '../utils/ids'
2-
import {
3-
type DocumentProjections,
4-
type DocumentProjectionValues,
5-
type ValidProjection,
6-
} from './types'
2+
import {type DocumentProjections, type DocumentProjectionValues} from './types'
3+
import {validateProjection} from './util'
74

85
export type ProjectionQueryResult = {
96
_id: string
@@ -18,7 +15,7 @@ interface CreateProjectionQueryResult {
1815
params: Record<string, unknown>
1916
}
2017

21-
type ProjectionMap = Record<string, {projection: ValidProjection; documentIds: Set<string>}>
18+
type ProjectionMap = Record<string, {projection: string; documentIds: Set<string>}>
2219

2320
export function createProjectionQuery(
2421
documentIds: Set<string>,
@@ -31,7 +28,7 @@ export function createProjectionQuery(
3128

3229
return Object.entries(projectionsForDoc).map(([projectionHash, projection]) => ({
3330
documentId: id,
34-
projection,
31+
projection: validateProjection(projection),
3532
projectionHash,
3633
}))
3734
})

packages/core/src/projection/resolveProjection.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {createSanityInstance, type SanityInstance} from '../store/createSanityIn
66
import {type StateSource} from '../store/createStateSourceAction'
77
import {getProjectionState} from './getProjectionState'
88
import {resolveProjection} from './resolveProjection'
9-
import {type ProjectionValuePending, type ValidProjection} from './types'
9+
import {type ProjectionValuePending} from './types'
1010

1111
vi.mock('./getProjectionState')
1212

@@ -35,7 +35,7 @@ describe('resolveProjection', () => {
3535
documentId: 'doc123',
3636
documentType: 'movie',
3737
})
38-
const projection = '{title}' as ValidProjection
38+
const projection = '{title}'
3939

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

packages/core/src/projection/resolveProjection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {bindActionByDataset} from '../store/createActionBinder'
55
import {type SanityInstance} from '../store/createSanityInstance'
66
import {getProjectionState, type ProjectionOptions} from './getProjectionState'
77
import {projectionStore} from './projectionStore'
8-
import {type ProjectionValuePending, type ValidProjection} from './types'
8+
import {type ProjectionValuePending} from './types'
99

1010
/** @beta */
1111
export function resolveProjection<
12-
TProjection extends ValidProjection = ValidProjection,
12+
TProjection extends string = string,
1313
TDocumentType extends string = string,
1414
TDataset extends string = string,
1515
TProjectId extends string = string,

packages/core/src/projection/types.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
/**
2-
* @public
3-
*/
4-
export type ValidProjection = `{${string}}`
5-
61
/**
72
* @public
83
* The result of a projection query
@@ -16,8 +11,16 @@ export interface DocumentProjectionValues<TValue extends object = object> {
1611
[projectionHash: string]: ProjectionValuePending<TValue>
1712
}
1813

14+
/**
15+
* @public
16+
* @deprecated
17+
* Template literals are a bit too limited, so this type is deprecated.
18+
* Use `string` instead. Projection strings are validated at runtime.
19+
*/
20+
export type ValidProjection = string
21+
1922
export interface DocumentProjections {
20-
[projectionHash: string]: ValidProjection
23+
[projectionHash: string]: string
2124
}
2225

2326
interface DocumentProjectionSubscriptions {
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import {type ValidProjection} from './types'
2-
31
export const PROJECTION_TAG = 'projection'
42
export const PROJECTION_PERSPECTIVE = 'raw'
53
export const PROJECTION_STATE_CLEAR_DELAY = 1000
@@ -9,11 +7,11 @@ export const STABLE_EMPTY_PROJECTION = {
97
isPending: false,
108
}
119

12-
export function validateProjection(projection: string): ValidProjection {
10+
export function validateProjection(projection: string): string {
1311
if (!projection.startsWith('{') || !projection.endsWith('}')) {
1412
throw new Error(
1513
`Invalid projection format: "${projection}". Projections must be enclosed in curly braces, e.g. "{title, 'author': author.name}"`,
1614
)
1715
}
18-
return projection as ValidProjection
16+
return projection
1917
}

packages/react/src/hooks/projection/useDocumentProjection.test.tsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
type DocumentHandle,
3-
getProjectionState,
4-
resolveProjection,
5-
type ValidProjection,
6-
} from '@sanity/sdk'
1+
import {type DocumentHandle, getProjectionState, resolveProjection} from '@sanity/sdk'
72
import {act, render, screen} from '@testing-library/react'
83
import {Suspense, useRef} from 'react'
94
import {type Mock} from 'vitest'
@@ -53,13 +48,7 @@ interface ProjectionResult {
5348
description: string
5449
}
5550

56-
function TestComponent({
57-
document,
58-
projection,
59-
}: {
60-
document: DocumentHandle
61-
projection: ValidProjection
62-
}) {
51+
function TestComponent({document, projection}: {document: DocumentHandle; projection: string}) {
6352
const ref = useRef(null)
6453
const {data, isPending} = useDocumentProjection<ProjectionResult>({...document, projection, ref})
6554

@@ -224,10 +213,7 @@ describe('useDocumentProjection', () => {
224213
const eventsUnsubscribe = vi.fn()
225214
subscribe.mockImplementation(() => eventsUnsubscribe)
226215

227-
function NoRefComponent({
228-
projection,
229-
...docHandle
230-
}: DocumentHandle & {projection: ValidProjection}) {
216+
function NoRefComponent({projection, ...docHandle}: DocumentHandle & {projection: string}) {
231217
const {data} = useDocumentProjection<ProjectionResult>({...docHandle, projection}) // No ref provided
232218
return (
233219
<div>
@@ -259,7 +245,7 @@ describe('useDocumentProjection', () => {
259245
function NonHtmlRefComponent({
260246
projection,
261247
...docHandle
262-
}: DocumentHandle & {projection: ValidProjection}) {
248+
}: DocumentHandle & {projection: string}) {
263249
const ref = useRef({}) // ref.current is not an HTML element
264250
const {data} = useDocumentProjection<ProjectionResult>({...docHandle, projection, ref})
265251
return (

0 commit comments

Comments
 (0)