@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.
- package/dist/index.d.ts +441 -322
- package/dist/index.js +1685 -1481
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
- package/src/_exports/index.ts +32 -30
- package/src/auth/authStore.test.ts +149 -104
- package/src/auth/authStore.ts +51 -100
- package/src/auth/handleAuthCallback.test.ts +67 -34
- package/src/auth/handleAuthCallback.ts +8 -7
- package/src/auth/logout.test.ts +61 -29
- package/src/auth/logout.ts +26 -28
- package/src/auth/refreshStampedToken.test.ts +197 -91
- package/src/auth/refreshStampedToken.ts +170 -59
- package/src/auth/subscribeToStateAndFetchCurrentUser.test.ts +5 -5
- package/src/auth/subscribeToStateAndFetchCurrentUser.ts +45 -47
- package/src/auth/subscribeToStorageEventsAndSetToken.test.ts +4 -5
- package/src/auth/subscribeToStorageEventsAndSetToken.ts +22 -24
- package/src/client/clientStore.test.ts +131 -67
- package/src/client/clientStore.ts +117 -116
- package/src/comlink/controller/actions/destroyController.test.ts +38 -13
- package/src/comlink/controller/actions/destroyController.ts +11 -15
- package/src/comlink/controller/actions/getOrCreateChannel.test.ts +56 -27
- package/src/comlink/controller/actions/getOrCreateChannel.ts +37 -35
- package/src/comlink/controller/actions/getOrCreateController.test.ts +27 -16
- package/src/comlink/controller/actions/getOrCreateController.ts +23 -22
- package/src/comlink/controller/actions/releaseChannel.test.ts +37 -13
- package/src/comlink/controller/actions/releaseChannel.ts +22 -21
- package/src/comlink/controller/comlinkControllerStore.test.ts +65 -36
- package/src/comlink/controller/comlinkControllerStore.ts +44 -5
- package/src/comlink/node/actions/getOrCreateNode.test.ts +31 -15
- package/src/comlink/node/actions/getOrCreateNode.ts +30 -29
- package/src/comlink/node/actions/releaseNode.test.ts +75 -55
- package/src/comlink/node/actions/releaseNode.ts +19 -21
- package/src/comlink/node/comlinkNodeStore.test.ts +6 -11
- package/src/comlink/node/comlinkNodeStore.ts +22 -5
- package/src/config/authConfig.ts +79 -0
- package/src/config/sanityConfig.ts +48 -0
- package/src/datasets/datasets.test.ts +2 -2
- package/src/datasets/datasets.ts +18 -5
- package/src/document/actions.test.ts +22 -10
- package/src/document/actions.ts +44 -56
- package/src/document/applyDocumentActions.test.ts +96 -36
- package/src/document/applyDocumentActions.ts +140 -99
- package/src/document/documentStore.test.ts +103 -155
- package/src/document/documentStore.ts +247 -238
- package/src/document/listen.ts +56 -55
- package/src/document/patchOperations.ts +0 -43
- package/src/document/permissions.test.ts +25 -12
- package/src/document/permissions.ts +11 -4
- package/src/document/processActions.test.ts +41 -8
- package/src/document/reducers.test.ts +87 -16
- package/src/document/reducers.ts +2 -2
- package/src/document/sharedListener.test.ts +34 -16
- package/src/document/sharedListener.ts +33 -11
- package/src/preview/getPreviewState.test.ts +40 -39
- package/src/preview/getPreviewState.ts +68 -56
- package/src/preview/previewConstants.ts +43 -0
- package/src/preview/previewQuery.test.ts +1 -1
- package/src/preview/previewQuery.ts +4 -5
- package/src/preview/previewStore.test.ts +13 -58
- package/src/preview/previewStore.ts +7 -21
- package/src/preview/resolvePreview.test.ts +33 -104
- package/src/preview/resolvePreview.ts +11 -21
- package/src/preview/subscribeToStateAndFetchBatches.test.ts +96 -97
- package/src/preview/subscribeToStateAndFetchBatches.ts +85 -81
- package/src/preview/util.ts +1 -0
- package/src/project/project.test.ts +3 -3
- package/src/project/project.ts +28 -5
- package/src/projection/getProjectionState.test.ts +188 -72
- package/src/projection/getProjectionState.ts +92 -62
- package/src/projection/projectionQuery.test.ts +114 -12
- package/src/projection/projectionQuery.ts +75 -32
- package/src/projection/projectionStore.test.ts +13 -51
- package/src/projection/projectionStore.ts +6 -43
- package/src/projection/resolveProjection.test.ts +32 -127
- package/src/projection/resolveProjection.ts +16 -28
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +203 -116
- package/src/projection/subscribeToStateAndFetchBatches.ts +140 -85
- package/src/projection/types.ts +50 -0
- package/src/projection/util.ts +3 -1
- package/src/projects/projects.test.ts +13 -4
- package/src/projects/projects.ts +6 -1
- package/src/query/queryStore.test.ts +10 -47
- package/src/query/queryStore.ts +151 -133
- package/src/query/queryStoreConstants.ts +2 -0
- package/src/store/createActionBinder.test.ts +153 -0
- package/src/store/createActionBinder.ts +176 -0
- package/src/store/createSanityInstance.test.ts +84 -0
- package/src/store/createSanityInstance.ts +124 -0
- package/src/store/createStateSourceAction.test.ts +196 -0
- package/src/store/createStateSourceAction.ts +260 -0
- package/src/store/createStoreInstance.test.ts +81 -0
- package/src/store/createStoreInstance.ts +80 -0
- package/src/store/createStoreState.test.ts +85 -0
- package/src/store/createStoreState.ts +92 -0
- package/src/store/defineStore.test.ts +18 -0
- package/src/store/defineStore.ts +81 -0
- package/src/users/reducers.test.ts +318 -0
- package/src/users/reducers.ts +88 -0
- package/src/users/types.ts +46 -4
- package/src/users/usersConstants.ts +4 -0
- package/src/users/usersStore.test.ts +350 -223
- package/src/users/usersStore.ts +285 -149
- package/src/utils/createFetcherStore.test.ts +6 -7
- package/src/utils/createFetcherStore.ts +150 -153
- package/src/utils/createGroqSearchFilter.test.ts +75 -0
- package/src/utils/createGroqSearchFilter.ts +85 -0
- package/src/{common/util.test.ts → utils/hashString.test.ts} +1 -1
- package/dist/index.cjs +0 -4888
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2121
- package/src/auth/fetchLoginUrls.test.ts +0 -163
- package/src/auth/fetchLoginUrls.ts +0 -74
- package/src/common/createLiveEventSubscriber.test.ts +0 -121
- package/src/common/createLiveEventSubscriber.ts +0 -55
- package/src/common/types.ts +0 -4
- package/src/instance/identity.test.ts +0 -46
- package/src/instance/identity.ts +0 -29
- package/src/instance/sanityInstance.test.ts +0 -77
- package/src/instance/sanityInstance.ts +0 -57
- package/src/instance/types.ts +0 -37
- package/src/preview/getPreviewProjection.ts +0 -45
- package/src/resources/README.md +0 -370
- package/src/resources/createAction.test.ts +0 -101
- package/src/resources/createAction.ts +0 -44
- package/src/resources/createResource.test.ts +0 -112
- package/src/resources/createResource.ts +0 -102
- package/src/resources/createStateSourceAction.test.ts +0 -114
- package/src/resources/createStateSourceAction.ts +0 -83
- package/src/resources/createStore.test.ts +0 -67
- package/src/resources/createStore.ts +0 -46
- package/src/store/createStore.test.ts +0 -108
- package/src/store/createStore.ts +0 -106
- /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
|
|
17
|
+
import {bindActionGlobally, type BoundStoreAction} from '../store/createActionBinder'
|
|
18
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
18
19
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from '../
|
|
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:
|
|
77
|
+
getState: BoundStoreAction<
|
|
78
78
|
FetcherStoreState<TParams, TData>,
|
|
79
79
|
TParams,
|
|
80
80
|
StateSource<TData | undefined>
|
|
81
81
|
>
|
|
82
|
-
resolveState:
|
|
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 =
|
|
111
|
+
const store = defineStore<FetcherStoreState<TParams, TData>>({
|
|
112
112
|
name,
|
|
113
113
|
getInitialState: () => ({
|
|
114
114
|
stateByParams: {},
|
|
115
115
|
}),
|
|
116
|
-
initialize() {
|
|
117
|
-
const subscription = subscribeToSubscriptionsAndFetch(
|
|
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 =
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
stateByParams
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
212
|
-
}),
|
|
213
|
-
)
|
|
204
|
+
return EMPTY
|
|
214
205
|
}),
|
|
215
|
-
)
|
|
216
|
-
),
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
|
|
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 =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
260
|
+
return {
|
|
261
|
+
stateByParams: {
|
|
262
|
+
...prev.stateByParams,
|
|
263
|
+
[key]: {
|
|
264
|
+
...entry,
|
|
265
|
+
subscriptions: newSubs,
|
|
266
|
+
},
|
|
266
267
|
},
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
})
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}, stateExpirationDelay)
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
)
|
|
274
275
|
|
|
275
|
-
const resolveState =
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
}
|