@sanity/sdk 0.0.0-chore-react-18-compat.1 → 0.0.0-chore-react-18-compat.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/index.d.ts +441 -322
  2. package/dist/index.js +1685 -1481
  3. package/dist/index.js.map +1 -1
  4. package/package.json +13 -15
  5. package/src/_exports/index.ts +32 -30
  6. package/src/auth/authStore.test.ts +149 -104
  7. package/src/auth/authStore.ts +51 -100
  8. package/src/auth/handleAuthCallback.test.ts +67 -34
  9. package/src/auth/handleAuthCallback.ts +8 -7
  10. package/src/auth/logout.test.ts +61 -29
  11. package/src/auth/logout.ts +26 -28
  12. package/src/auth/refreshStampedToken.test.ts +197 -91
  13. package/src/auth/refreshStampedToken.ts +170 -59
  14. package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
  15. package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
  16. package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
  17. package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
  18. package/src/client/clientStore.test.ts +131 -67
  19. package/src/client/clientStore.ts +117 -116
  20. package/src/comlink/controller/actions/destroyController.test.ts +38 -13
  21. package/src/comlink/controller/actions/destroyController.ts +11 -15
  22. package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
  23. package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
  24. package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
  25. package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
  26. package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
  27. package/src/comlink/controller/actions/releaseChannel.ts +22 -21
  28. package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
  29. package/src/comlink/controller/comlinkControllerStore.ts +44 -5
  30. package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
  31. package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
  32. package/src/comlink/node/actions/releaseNode.test.ts +75 -55
  33. package/src/comlink/node/actions/releaseNode.ts +19 -21
  34. package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
  35. package/src/comlink/node/comlinkNodeStore.ts +22 -5
  36. package/src/config/authConfig.ts +79 -0
  37. package/src/config/sanityConfig.ts +48 -0
  38. package/src/datasets/datasets.test.ts +2 -2
  39. package/src/datasets/datasets.ts +18 -5
  40. package/src/document/actions.test.ts +22 -10
  41. package/src/document/actions.ts +44 -56
  42. package/src/document/applyDocumentActions.test.ts +96 -36
  43. package/src/document/applyDocumentActions.ts +140 -99
  44. package/src/document/documentStore.test.ts +103 -155
  45. package/src/document/documentStore.ts +247 -238
  46. package/src/document/listen.ts +56 -55
  47. package/src/document/patchOperations.ts +0 -43
  48. package/src/document/permissions.test.ts +25 -12
  49. package/src/document/permissions.ts +11 -4
  50. package/src/document/processActions.test.ts +41 -8
  51. package/src/document/reducers.test.ts +87 -16
  52. package/src/document/reducers.ts +2 -2
  53. package/src/document/sharedListener.test.ts +34 -16
  54. package/src/document/sharedListener.ts +33 -11
  55. package/src/preview/getPreviewState.test.ts +40 -39
  56. package/src/preview/getPreviewState.ts +68 -56
  57. package/src/preview/previewConstants.ts +43 -0
  58. package/src/preview/previewQuery.test.ts +1 -1
  59. package/src/preview/previewQuery.ts +4 -5
  60. package/src/preview/previewStore.test.ts +13 -58
  61. package/src/preview/previewStore.ts +7 -21
  62. package/src/preview/resolvePreview.test.ts +33 -104
  63. package/src/preview/resolvePreview.ts +11 -21
  64. package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
  65. package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
  66. package/src/preview/util.ts +1 -0
  67. package/src/project/project.test.ts +3 -3
  68. package/src/project/project.ts +28 -5
  69. package/src/projection/getProjectionState.test.ts +188 -72
  70. package/src/projection/getProjectionState.ts +92 -62
  71. package/src/projection/projectionQuery.test.ts +114 -12
  72. package/src/projection/projectionQuery.ts +75 -32
  73. package/src/projection/projectionStore.test.ts +13 -51
  74. package/src/projection/projectionStore.ts +6 -43
  75. package/src/projection/resolveProjection.test.ts +32 -127
  76. package/src/projection/resolveProjection.ts +16 -28
  77. package/src/projection/subscribeToStateAndFetchBatches.test.ts +203 -116
  78. package/src/projection/subscribeToStateAndFetchBatches.ts +140 -85
  79. package/src/projection/types.ts +50 -0
  80. package/src/projection/util.ts +3 -1
  81. package/src/projects/projects.test.ts +13 -4
  82. package/src/projects/projects.ts +6 -1
  83. package/src/query/queryStore.test.ts +10 -47
  84. package/src/query/queryStore.ts +151 -133
  85. package/src/query/queryStoreConstants.ts +2 -0
  86. package/src/store/createActionBinder.test.ts +153 -0
  87. package/src/store/createActionBinder.ts +176 -0
  88. package/src/store/createSanityInstance.test.ts +84 -0
  89. package/src/store/createSanityInstance.ts +124 -0
  90. package/src/store/createStateSourceAction.test.ts +196 -0
  91. package/src/store/createStateSourceAction.ts +260 -0
  92. package/src/store/createStoreInstance.test.ts +81 -0
  93. package/src/store/createStoreInstance.ts +80 -0
  94. package/src/store/createStoreState.test.ts +85 -0
  95. package/src/store/createStoreState.ts +92 -0
  96. package/src/store/defineStore.test.ts +18 -0
  97. package/src/store/defineStore.ts +81 -0
  98. package/src/users/reducers.test.ts +318 -0
  99. package/src/users/reducers.ts +88 -0
  100. package/src/users/types.ts +46 -4
  101. package/src/users/usersConstants.ts +4 -0
  102. package/src/users/usersStore.test.ts +350 -223
  103. package/src/users/usersStore.ts +285 -149
  104. package/src/utils/createFetcherStore.test.ts +6 -7
  105. package/src/utils/createFetcherStore.ts +150 -153
  106. package/src/utils/createGroqSearchFilter.test.ts +75 -0
  107. package/src/utils/createGroqSearchFilter.ts +85 -0
  108. package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
  109. package/dist/index.cjs +0 -4888
  110. package/dist/index.cjs.map +0 -1
  111. package/dist/index.d.cts +0 -2121
  112. package/src/auth/fetchLoginUrls.test.ts +0 -163
  113. package/src/auth/fetchLoginUrls.ts +0 -74
  114. package/src/common/createLiveEventSubscriber.test.ts +0 -121
  115. package/src/common/createLiveEventSubscriber.ts +0 -55
  116. package/src/common/types.ts +0 -4
  117. package/src/instance/identity.test.ts +0 -46
  118. package/src/instance/identity.ts +0 -29
  119. package/src/instance/sanityInstance.test.ts +0 -77
  120. package/src/instance/sanityInstance.ts +0 -57
  121. package/src/instance/types.ts +0 -37
  122. package/src/preview/getPreviewProjection.ts +0 -45
  123. package/src/resources/README.md +0 -370
  124. package/src/resources/createAction.test.ts +0 -101
  125. package/src/resources/createAction.ts +0 -44
  126. package/src/resources/createResource.test.ts +0 -112
  127. package/src/resources/createResource.ts +0 -102
  128. package/src/resources/createStateSourceAction.test.ts +0 -114
  129. package/src/resources/createStateSourceAction.ts +0 -83
  130. package/src/resources/createStore.test.ts +0 -67
  131. package/src/resources/createStore.ts +0 -46
  132. package/src/store/createStore.test.ts +0 -108
  133. package/src/store/createStore.ts +0 -106
  134. /package/src/{common/util.ts → utils/hashString.ts} +0 -0
@@ -14,15 +14,14 @@ import {
14
14
  tap,
15
15
  } from 'rxjs/operators'
16
16
 
17
- import {type SanityInstance} from '../instance/types'
17
+ import {bindActionGlobally, type BoundStoreAction} from '../store/createActionBinder'
18
+ import {type SanityInstance} from '../store/createSanityInstance'
18
19
  import {
19
- type ActionContext,
20
- createAction,
21
- createInternalAction,
22
- type ResourceAction,
23
- } from '../resources/createAction'
24
- import {createResource} from '../resources/createResource'
25
- import {createStateSourceAction, type StateSource} from '../resources/createStateSourceAction'
20
+ createStateSourceAction,
21
+ type SelectorContext,
22
+ type StateSource,
23
+ } from '../store/createStateSourceAction'
24
+ import {defineStore, type StoreContext} from '../store/defineStore'
26
25
  import {insecureRandomId} from '../utils/ids'
27
26
 
28
27
  interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
@@ -38,7 +37,7 @@ interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
38
37
  * The function used to convert the params into keys that state related to
39
38
  * those params will be stored.
40
39
  */
41
- getKey: (...params: TParams) => string
40
+ getKey: (instance: SanityInstance, ...params: TParams) => string
42
41
  /**
43
42
  * Delay in ms before clearing state after the last subscription is removed.
44
43
  * This results in react components suspending again due to no previous state
@@ -53,6 +52,7 @@ interface CreateFetcherStoreOptions<TParams extends unknown[], TData> {
53
52
 
54
53
  interface StoreEntry<TParams extends unknown[], TData> {
55
54
  params: TParams
55
+ instance: SanityInstance
56
56
  key: string
57
57
  data?: TData
58
58
  error?: unknown
@@ -74,12 +74,12 @@ export interface FetcherStoreState<TParams extends unknown[], TData> {
74
74
  * @public
75
75
  */
76
76
  export interface FetcherStore<TParams extends unknown[], TData> {
77
- getState: ResourceAction<
77
+ getState: BoundStoreAction<
78
78
  FetcherStoreState<TParams, TData>,
79
79
  TParams,
80
80
  StateSource<TData | undefined>
81
81
  >
82
- resolveState: ResourceAction<FetcherStoreState<TParams, TData>, TParams, Promise<TData>>
82
+ resolveState: BoundStoreAction<FetcherStoreState<TParams, TData>, TParams, Promise<TData>>
83
83
  }
84
84
 
85
85
  /**
@@ -108,13 +108,13 @@ export function createFetcherStore<TParams extends unknown[], TData>({
108
108
  fetchThrottleInternal = 1000,
109
109
  stateExpirationDelay = 5000,
110
110
  }: CreateFetcherStoreOptions<TParams, TData>): FetcherStore<TParams, TData> {
111
- const store = createResource<FetcherStoreState<TParams, TData>>({
111
+ const store = defineStore<FetcherStoreState<TParams, TData>>({
112
112
  name,
113
113
  getInitialState: () => ({
114
114
  stateByParams: {},
115
115
  }),
116
- initialize() {
117
- const subscription = subscribeToSubscriptionsAndFetch(this)
116
+ initialize: (context) => {
117
+ const subscription = subscribeToSubscriptionsAndFetch(context)
118
118
  return () => subscription.unsubscribe()
119
119
  },
120
120
  })
@@ -125,160 +125,157 @@ export function createFetcherStore<TParams extends unknown[], TData>({
125
125
  * and if enough time has elapsed since the last fetch, we update the timestamp
126
126
  * and call the factory function for that key.
127
127
  */
128
- const subscribeToSubscriptionsAndFetch = createInternalAction(
129
- ({instance, state}: ActionContext<FetcherStoreState<TParams, TData>>) => {
130
- return function () {
131
- const factoryFn = getObservable(instance)
128
+ const subscribeToSubscriptionsAndFetch = ({
129
+ state,
130
+ }: StoreContext<FetcherStoreState<TParams, TData>>) => {
131
+ return state.observable
132
+ .pipe(
133
+ // Map the state to an array of [serialized, entry] pairs.
134
+ switchMap((s: FetcherStoreState<TParams, TData>) => {
135
+ const entries = Object.entries(s.stateByParams)
136
+ return entries.length > 0 ? from(entries) : EMPTY
137
+ }),
138
+ // Group by the serialized key.
139
+ groupBy(([key]) => key),
140
+ mergeMap((group$) =>
141
+ group$.pipe(
142
+ // Emit an initial value for pairwise comparisons.
143
+ startWith<[string, StoreEntry<TParams, TData> | undefined]>([group$.key, undefined]),
144
+ pairwise(),
145
+ // Trigger only when the subscriptions array grows.
146
+ filter(([[, prevEntry], [, currEntry]]) => {
147
+ const prevSubs = prevEntry?.subscriptions ?? []
148
+ const currSubs = currEntry?.subscriptions ?? []
149
+ return currSubs.length > prevSubs.length
150
+ }),
151
+ map(([, [, currEntry]]) => currEntry),
132
152
 
133
- return state.observable
134
- .pipe(
135
- // Map the state to an array of [serialized, entry] pairs.
136
- switchMap((s: FetcherStoreState<TParams, TData>) => {
137
- const entries = Object.entries(s.stateByParams)
138
- return entries.length > 0 ? from(entries) : EMPTY
153
+ // Only trigger if we haven't fetched recently.
154
+ filter((entry) => {
155
+ const lastFetch = entry?.lastFetchInitiatedAt
156
+ if (!lastFetch) return true
157
+ return Date.now() - new Date(lastFetch).getTime() >= fetchThrottleInternal
139
158
  }),
140
- // Group by the serialized key.
141
- groupBy(([key]) => key),
142
- mergeMap((group$) =>
143
- group$.pipe(
144
- // Emit an initial value for pairwise comparisons.
145
- startWith<[string, StoreEntry<TParams, TData> | undefined]>([
146
- group$.key,
147
- undefined,
148
- ]),
149
- pairwise(),
150
- // Trigger only when the subscriptions array grows.
151
- filter(([[, prevEntry], [, currEntry]]) => {
152
- const prevSubs = prevEntry?.subscriptions ?? []
153
- const currSubs = currEntry?.subscriptions ?? []
154
- return currSubs.length > prevSubs.length
155
- }),
156
- map(([, [, currEntry]]) => currEntry),
159
+ switchMap((entry) => {
160
+ // Retrieve params from the entry
161
+ if (!entry) return EMPTY
157
162
 
158
- // Only trigger if we haven't fetched recently.
159
- filter((entry) => {
160
- const lastFetch = entry?.lastFetchInitiatedAt
161
- if (!lastFetch) return true
162
- return Date.now() - new Date(lastFetch).getTime() >= fetchThrottleInternal
163
- }),
164
- switchMap((entry) => {
165
- // Retrieve params from the entry
166
- if (!entry) return EMPTY
163
+ // Record that a fetch is being initiated.
164
+ state.set('setLastFetchInitiatedAt', (prev: FetcherStoreState<TParams, TData>) => ({
165
+ stateByParams: {
166
+ ...prev.stateByParams,
167
+ [entry.key]: {
168
+ ...entry,
169
+ ...prev.stateByParams[entry.key],
170
+ lastFetchInitiatedAt: new Date().toISOString(),
171
+ },
172
+ },
173
+ }))
167
174
 
168
- // Record that a fetch is being initiated.
169
- state.set(
170
- 'setLastFetchInitiatedAt',
171
- (prev: FetcherStoreState<TParams, TData>) => ({
172
- stateByParams: {
173
- ...prev.stateByParams,
174
- [entry.key]: {
175
- ...entry,
176
- ...prev.stateByParams[entry.key],
177
- lastFetchInitiatedAt: new Date().toISOString(),
178
- },
175
+ const factoryFn = getObservable(entry.instance)
176
+ return factoryFn(...entry.params).pipe(
177
+ // the `createStateSourceAction` util requires the update
178
+ // to
179
+ delay(0, asapScheduler),
180
+ tap((data: TData) =>
181
+ state.set('setData', (prev: FetcherStoreState<TParams, TData>) => ({
182
+ stateByParams: {
183
+ ...prev.stateByParams,
184
+ [entry.key]: {
185
+ ...omit(entry, 'error'),
186
+ ...omit(prev.stateByParams[entry.key], 'error'),
187
+ data,
179
188
  },
180
- }),
181
- )
182
-
183
- return factoryFn(...entry.params).pipe(
184
- // the `createStateSourceAction` util requires the update
185
- // to
186
- delay(0, asapScheduler),
187
- tap((data: TData) =>
188
- state.set('setData', (prev: FetcherStoreState<TParams, TData>) => ({
189
- stateByParams: {
190
- ...prev.stateByParams,
191
- [entry.key]: {
192
- ...omit(entry, 'error'),
193
- ...omit(prev.stateByParams[entry.key], 'error'),
194
- data,
195
- },
196
- },
197
- })),
198
- ),
199
- catchError((error) => {
200
- state.set('setError', (prev) => ({
201
- stateByParams: {
202
- ...prev.stateByParams,
203
- [entry.key]: {
204
- ...entry,
205
- ...prev.stateByParams[entry.key],
206
- error,
207
- },
208
- },
209
- }))
189
+ },
190
+ })),
191
+ ),
192
+ catchError((error) => {
193
+ state.set('setError', (prev) => ({
194
+ stateByParams: {
195
+ ...prev.stateByParams,
196
+ [entry.key]: {
197
+ ...entry,
198
+ ...prev.stateByParams[entry.key],
199
+ error,
200
+ },
201
+ },
202
+ }))
210
203
 
211
- return EMPTY
212
- }),
213
- )
204
+ return EMPTY
214
205
  }),
215
- ),
216
- ),
217
- )
218
- .subscribe({
219
- error: (error) => state.set('setError', {error}),
220
- })
221
- }
222
- },
223
- )
206
+ )
207
+ }),
208
+ ),
209
+ ),
210
+ )
211
+ .subscribe({
212
+ error: (error) => state.set('setError', {error}),
213
+ })
214
+ }
224
215
 
225
- const getState = createStateSourceAction(store, {
226
- selector: ({stateByParams, error}: FetcherStoreState<TParams, TData>, ...params: TParams) => {
227
- if (error) throw error
228
- const key = getKey(...params)
229
- const entry = stateByParams[key]
230
- if (entry?.error) throw entry.error
231
- return entry?.data
232
- },
233
- onSubscribe: ({state}, ...params: TParams) => {
234
- const subscriptionId = insecureRandomId()
235
- const key = getKey(...params)
216
+ const getState = bindActionGlobally(
217
+ store,
218
+ createStateSourceAction({
219
+ selector: (
220
+ {
221
+ instance,
222
+ state: {stateByParams, error},
223
+ }: SelectorContext<FetcherStoreState<TParams, TData>>,
224
+ ...params: TParams
225
+ ) => {
226
+ if (error) throw error
227
+ const key = getKey(instance, ...params)
228
+ const entry = stateByParams[key]
229
+ if (entry?.error) throw entry.error
230
+ return entry?.data
231
+ },
232
+ onSubscribe: ({instance, state}, ...params: TParams) => {
233
+ const subscriptionId = insecureRandomId()
234
+ const key = getKey(instance, ...params)
236
235
 
237
- state.set('addSubscription', (prev: FetcherStoreState<TParams, TData>) => ({
238
- stateByParams: {
239
- ...prev.stateByParams,
240
- [key]: {
241
- ...prev.stateByParams[key],
242
- key,
243
- params: prev.stateByParams[key]?.params || params,
244
- subscriptions: [...(prev.stateByParams[key]?.subscriptions || []), subscriptionId],
236
+ state.set('addSubscription', (prev: FetcherStoreState<TParams, TData>) => ({
237
+ stateByParams: {
238
+ ...prev.stateByParams,
239
+ [key]: {
240
+ ...prev.stateByParams[key],
241
+ instance,
242
+ key,
243
+ params: prev.stateByParams[key]?.params || params,
244
+ subscriptions: [...(prev.stateByParams[key]?.subscriptions || []), subscriptionId],
245
+ },
245
246
  },
246
- },
247
- }))
247
+ }))
248
248
 
249
- return () => {
250
- setTimeout(() => {
251
- state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
252
- const entry = prev.stateByParams[key]
253
- if (!entry) return prev
249
+ return () => {
250
+ setTimeout(() => {
251
+ state.set('removeSubscription', (prev: FetcherStoreState<TParams, TData>) => {
252
+ const entry = prev.stateByParams[key]
253
+ if (!entry) return prev
254
254
 
255
- const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
256
- if (newSubs.length === 0) {
257
- return {stateByParams: omit(prev.stateByParams, key)}
258
- }
255
+ const newSubs = (entry.subscriptions || []).filter((id) => id !== subscriptionId)
256
+ if (newSubs.length === 0) {
257
+ return {stateByParams: omit(prev.stateByParams, key)}
258
+ }
259
259
 
260
- return {
261
- stateByParams: {
262
- ...prev.stateByParams,
263
- [key]: {
264
- ...entry,
265
- subscriptions: newSubs,
260
+ return {
261
+ stateByParams: {
262
+ ...prev.stateByParams,
263
+ [key]: {
264
+ ...entry,
265
+ subscriptions: newSubs,
266
+ },
266
267
  },
267
- },
268
- }
269
- })
270
- }, stateExpirationDelay)
271
- }
272
- },
273
- })
268
+ }
269
+ })
270
+ }, stateExpirationDelay)
271
+ }
272
+ },
273
+ }),
274
+ )
274
275
 
275
- const resolveState = createAction(store, () => {
276
- return function (...params: TParams) {
277
- return firstValueFrom(
278
- getState(this, ...params).observable.pipe(first((i) => i !== undefined)),
279
- )
280
- }
281
- })
276
+ const resolveState = bindActionGlobally(store, ({instance}, ...params: TParams) =>
277
+ firstValueFrom(getState(instance, ...params).observable.pipe(first((i) => i !== undefined))),
278
+ )
282
279
 
283
280
  return {getState, resolveState}
284
281
  }
@@ -0,0 +1,75 @@
1
+ import {describe, expect, test} from 'vitest'
2
+
3
+ import {createGroqSearchFilter} from './createGroqSearchFilter'
4
+
5
+ describe('createGroqSearchFilter', () => {
6
+ test('should create filter with wildcard added to the last token', () => {
7
+ expect(createGroqSearchFilter('hello world')).toBe('[@] match text::query("hello world*")')
8
+ })
9
+
10
+ test('should create filter with wildcard added to the last non-negated token', () => {
11
+ expect(createGroqSearchFilter('hello -world')).toBe('[@] match text::query("hello* -world")')
12
+ })
13
+
14
+ test('should create filter with wildcard before an exact match phrase', () => {
15
+ expect(createGroqSearchFilter('hello "exact match"')).toBe(
16
+ '[@] match text::query("hello* \\"exact match\\"")',
17
+ )
18
+ })
19
+
20
+ test('should create filter without adding wildcard if the last eligible token already has one', () => {
21
+ expect(createGroqSearchFilter('hello world*')).toBe('[@] match text::query("hello world*")')
22
+ })
23
+
24
+ test('should create filter without wildcard if the only token is negated', () => {
25
+ expect(createGroqSearchFilter('-negated')).toBe('[@] match text::query("-negated")')
26
+ })
27
+
28
+ test('should create filter without wildcard if the only token is an exact phrase', () => {
29
+ expect(createGroqSearchFilter('"exact phrase"')).toBe(
30
+ '[@] match text::query("\\"exact phrase\\"")',
31
+ )
32
+ })
33
+
34
+ test('should return empty string for empty input', () => {
35
+ expect(createGroqSearchFilter('')).toBe('')
36
+ })
37
+
38
+ test('should return empty string for whitespace input', () => {
39
+ expect(createGroqSearchFilter(' ')).toBe('')
40
+ })
41
+
42
+ test('should handle leading whitespace', () => {
43
+ expect(createGroqSearchFilter(' leading space')).toBe('[@] match text::query("leading space*")')
44
+ })
45
+
46
+ test('should handle trailing whitespace', () => {
47
+ expect(createGroqSearchFilter('trailing space ')).toBe(
48
+ '[@] match text::query("trailing space*")',
49
+ )
50
+ })
51
+
52
+ test('should handle multiple spaces between tokens', () => {
53
+ expect(createGroqSearchFilter('multiple spaces')).toBe(
54
+ '[@] match text::query("multiple spaces*")',
55
+ )
56
+ })
57
+
58
+ test('should handle mixed token types', () => {
59
+ expect(createGroqSearchFilter('term1 "exact phrase" -negated term2')).toBe(
60
+ '[@] match text::query("term1 \\"exact phrase\\" -negated term2*")',
61
+ )
62
+ })
63
+
64
+ test('should handle mixed token types ending with negation', () => {
65
+ expect(createGroqSearchFilter('term1 "exact phrase" term2 -negated')).toBe(
66
+ '[@] match text::query("term1 \\"exact phrase\\" term2* -negated")',
67
+ )
68
+ })
69
+
70
+ test('should handle mixed token types ending with exact match', () => {
71
+ expect(createGroqSearchFilter('term1 -negated term2 "exact phrase"')).toBe(
72
+ '[@] match text::query("term1 -negated term2* \\"exact phrase\\"")',
73
+ )
74
+ })
75
+ })
@@ -0,0 +1,85 @@
1
+ const WILDCARD_TOKEN = '*'
2
+ const NEGATION_TOKEN = '-'
3
+ // This regex handles simple cases including quoted phrases.
4
+ // More complex query syntaxes might need a more robust parser.
5
+ const TOKEN_REGEX = /(?:[^\s"]+|"[^"]*")+/g
6
+
7
+ /**
8
+ * @internal
9
+ * Checks if a token starts with the negation character.
10
+ */
11
+ function isNegationToken(token: string | undefined): boolean {
12
+ return typeof token !== 'undefined' && token.trim().startsWith(NEGATION_TOKEN)
13
+ }
14
+
15
+ /**
16
+ * @internal
17
+ * Checks if a token ends with the wildcard character.
18
+ */
19
+ function isPrefixToken(token: string | undefined): boolean {
20
+ return typeof token !== 'undefined' && token.trim().endsWith(WILDCARD_TOKEN)
21
+ }
22
+
23
+ /**
24
+ * @internal
25
+ * Checks if a token is enclosed in double quotes.
26
+ */
27
+ function isExactMatchToken(token: string | undefined): boolean {
28
+ // Ensure the token exists, has at least 2 characters, and starts/ends with "
29
+ return !!token && token.length >= 2 && token.startsWith('"') && token.endsWith('"')
30
+ }
31
+
32
+ /**
33
+ * Creates a GROQ search filter string (`[@] match text::query("...")`)
34
+ * from a raw search query string.
35
+ *
36
+ * It applies wildcard ('*') logic to the last eligible token and escapes
37
+ * double quotes within the search term.
38
+ *
39
+ * If the input query is empty or only whitespace, it returns an empty string.
40
+ *
41
+ * @param query - The raw input search string.
42
+ * @returns The GROQ search filter string, or an empty string.
43
+ * @internal
44
+ */
45
+ export function createGroqSearchFilter(query: string): string {
46
+ // Trim leading/trailing whitespace from the overall query first
47
+ const trimmedQuery = query.trim()
48
+ if (!trimmedQuery) {
49
+ return '' // Return empty if query is empty or just whitespace
50
+ }
51
+
52
+ // Extract tokens using the regex
53
+ const tokens = trimmedQuery.match(TOKEN_REGEX) ?? []
54
+
55
+ // Find the index of the last token eligible for wildcard appending
56
+ const reversedTokens = [...tokens].reverse()
57
+ const reversedIndex = reversedTokens.findIndex(
58
+ (token: string) => !isNegationToken(token) && !isExactMatchToken(token),
59
+ )
60
+ const finalIncrementalTokenIndex = reversedIndex === -1 ? -1 : tokens.length - 1 - reversedIndex
61
+
62
+ // Get the actual token based on the found index
63
+ const finalIncrementalToken = tokens[finalIncrementalTokenIndex]
64
+
65
+ const processedTokens = [...tokens]
66
+ // If a suitable token was found and it doesn't already end with a wildcard,
67
+ // apply the wildcard.
68
+ if (finalIncrementalToken !== undefined && !isPrefixToken(finalIncrementalToken)) {
69
+ // Replace the identified token with its wildcarded version
70
+ processedTokens.splice(
71
+ finalIncrementalTokenIndex,
72
+ 1,
73
+ `${finalIncrementalToken}${WILDCARD_TOKEN}`,
74
+ )
75
+ }
76
+
77
+ // Join the tokens back into a space-separated string
78
+ const wildcardSearch = processedTokens.join(' ')
79
+
80
+ // Escape double quotes within the final search term for the GROQ query
81
+ const escapedSearch = wildcardSearch.replace(/"/g, '\\"')
82
+
83
+ // Construct the final GROQ filter clause
84
+ return `[@] match text::query("${escapedSearch}")`
85
+ }
@@ -1,6 +1,6 @@
1
1
  import {describe, expect, it} from 'vitest'
2
2
 
3
- import {hashString} from './util'
3
+ import {hashString} from './hashString'
4
4
 
5
5
  describe('hashString', () => {
6
6
  it('should generate consistent hashes for the same input', () => {