@sanity/sdk 2.1.0 → 2.1.1

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.
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Filter criteria for intent matching. Can be combined to create more specific intents.
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // matches only geopoints in the travel-project project, production dataset
7
+ * const filter: IntentFilter = {
8
+ * projectId: 'travel-project',
9
+ * dataset: 'production',
10
+ * types: ['geopoint']
11
+ * }
12
+ *
13
+ * // matches all documents in the travel-project project
14
+ * const filter: IntentFilter = {
15
+ * projectId: 'travel-project',
16
+ * types: ['*']
17
+ * }
18
+ *
19
+ * // matches geopoints in the travel-project production dataset and map pins in all projects in the org
20
+ * const filters: IntentFilter[] = [
21
+ * {
22
+ * projectId: 'travel-project',
23
+ * dataset: 'production',
24
+ * types: ['geopoint']
25
+ * },
26
+ * {
27
+ * types: ['map-pin']
28
+ * }
29
+ * ]
30
+ * ```
31
+ * @public
32
+ */
33
+ export interface IntentFilter {
34
+ /**
35
+ * Project ID to match against
36
+ * @remarks When specified, the intent will only match for the specified project.
37
+ */
38
+ projectId?: string
39
+
40
+ /**
41
+ * Dataset to match against
42
+ * @remarks When specified, the intent will only match for the specified dataset. Requires projectId to be specified.
43
+ */
44
+ dataset?: string
45
+
46
+ /**
47
+ * Document types that this intent can handle
48
+ * @remarks This is required for all filters. Use ['*'] to match all document types.
49
+ */
50
+ types: string[]
51
+ }
52
+
53
+ /**
54
+ * Intent definition structure for registering user intents
55
+ * @public
56
+ */
57
+ export interface Intent {
58
+ /**
59
+ * Unique identifier for this intent
60
+ * @remarks Should be unique across all registered intents in an org for proper matching
61
+ */
62
+ id: string
63
+
64
+ /**
65
+ * The action that this intent performs
66
+ * @remarks Examples: "view", "edit", "create", "delete"
67
+ */
68
+ action: 'view' | 'edit' | 'create' | 'delete'
69
+
70
+ /**
71
+ * Human-readable title for this intent
72
+ * @remarks Used for display purposes in UI or logs
73
+ */
74
+ title: string
75
+
76
+ /**
77
+ * Detailed description of what this intent does
78
+ * @remarks Helps users understand the purpose and behavior of the intent
79
+ */
80
+ description?: string
81
+
82
+ /**
83
+ * Array of filter criteria for intent matching
84
+ * @remarks At least one filter is required. Use `{types: ['*']}` to match everything
85
+ */
86
+ filters: IntentFilter[]
87
+ }
88
+
89
+ /**
90
+ * Creates a properly typed intent definition for registration with the backend.
91
+ *
92
+ * This utility function provides TypeScript support and validation for intent declarations.
93
+ * It is also used in the CLI if intents are declared as bare objects in an intents file.
94
+ *
95
+ * @param intent - The intent definition object
96
+ * @returns The same intent object with proper typing
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * // Specific filter for a document type
101
+ * const viewGeopointInMapApp = defineIntent({
102
+ * id: 'viewGeopointInMapApp',
103
+ * action: 'view',
104
+ * title: 'View a geopoint in the map app',
105
+ * description: 'This lets you view a geopoint in the map app',
106
+ * filters: [
107
+ * {
108
+ * projectId: 'travel-project',
109
+ * dataset: 'production',
110
+ * types: ['geopoint']
111
+ * }
112
+ * ]
113
+ * })
114
+ *
115
+ * export default viewGeopointInMapApp
116
+ * ```
117
+ *
118
+ * If your intent is asynchronous, resolve the promise before defining / returning the intent
119
+ * ```typescript
120
+ * async function createAsyncIntent() {
121
+ * const currentProject = await asyncProjectFunction()
122
+ * const currentDataset = await asyncDatasetFunction()
123
+ *
124
+ * return defineIntent({
125
+ * id: 'dynamicIntent',
126
+ * action: 'view',
127
+ * title: 'Dynamic Intent',
128
+ * description: 'Intent with dynamically resolved values',
129
+ * filters: [
130
+ * {
131
+ * projectId: currentProject, // Resolved value
132
+ * dataset: currentDataset, // Resolved value
133
+ * types: ['document']
134
+ * }
135
+ * ]
136
+ * })
137
+ * }
138
+ *
139
+ * const intent = await createAsyncIntent()
140
+ * export default intent
141
+ * ```
142
+ *
143
+ * @public
144
+ */
145
+ export function defineIntent(intent: Intent): Intent {
146
+ // Validate required fields
147
+ if (!intent.id) {
148
+ throw new Error('Intent must have an id')
149
+ }
150
+ if (!intent.action) {
151
+ throw new Error('Intent must have an action')
152
+ }
153
+ if (!intent.title) {
154
+ throw new Error('Intent must have a title')
155
+ }
156
+ if (!Array.isArray(intent.filters)) {
157
+ throw new Error('Intent must have a filters array')
158
+ }
159
+ if (intent.filters.length === 0) {
160
+ throw new Error(
161
+ "Intent must have at least one filter. If you want to match everything, use {types: ['*']}",
162
+ )
163
+ }
164
+
165
+ // Validate each filter
166
+ intent.filters.forEach((filter, index) => {
167
+ validateFilter(filter, index)
168
+ })
169
+
170
+ // Return the intent as-is, providing type safety and runtime validation
171
+ return intent
172
+ }
173
+
174
+ /**
175
+ * Validates an individual filter object
176
+ * @param filter - The filter to validate
177
+ * @param index - The filter's index in the array (for error messages)
178
+ * @internal
179
+ */
180
+ function validateFilter(filter: IntentFilter, index: number): void {
181
+ const filterContext = `Filter at index ${index}`
182
+
183
+ // Check that filter is an object
184
+ if (!filter || typeof filter !== 'object') {
185
+ throw new Error(`${filterContext} must be an object`)
186
+ }
187
+
188
+ // Check that types is required
189
+ if (filter.types === undefined) {
190
+ throw new Error(
191
+ `${filterContext} must have a types property. Use ['*'] to match all document types.`,
192
+ )
193
+ }
194
+
195
+ // Validate projectId
196
+ if (filter.projectId !== undefined) {
197
+ if (typeof filter.projectId !== 'string') {
198
+ throw new Error(`${filterContext}: projectId must be a string`)
199
+ }
200
+ if (filter.projectId.trim() === '') {
201
+ throw new Error(`${filterContext}: projectId cannot be empty`)
202
+ }
203
+ }
204
+
205
+ // Validate dataset
206
+ if (filter.dataset !== undefined) {
207
+ if (typeof filter.dataset !== 'string') {
208
+ throw new Error(`${filterContext}: dataset must be a string`)
209
+ }
210
+ if (filter.dataset.trim() === '') {
211
+ throw new Error(`${filterContext}: dataset cannot be empty`)
212
+ }
213
+ // Dataset requires projectId to be specified
214
+ if (filter.projectId === undefined) {
215
+ throw new Error(`${filterContext}: dataset cannot be specified without projectId`)
216
+ }
217
+ }
218
+
219
+ // Validate types (now required)
220
+ if (!Array.isArray(filter.types)) {
221
+ throw new Error(`${filterContext}: types must be an array`)
222
+ }
223
+ if (filter.types.length === 0) {
224
+ throw new Error(`${filterContext}: types array cannot be empty`)
225
+ }
226
+
227
+ // Validate each type
228
+ filter.types.forEach((type, typeIndex) => {
229
+ if (typeof type !== 'string') {
230
+ throw new Error(`${filterContext}: types[${typeIndex}] must be a string`)
231
+ }
232
+ if (type.trim() === '') {
233
+ throw new Error(`${filterContext}: types[${typeIndex}] cannot be empty`)
234
+ }
235
+ })
236
+
237
+ // Check for wildcard exclusivity
238
+ const hasWildcard = filter.types.includes('*')
239
+ if (hasWildcard && filter.types.length > 1) {
240
+ throw new Error(
241
+ `${filterContext}: when using wildcard '*', it must be the only type in the array`,
242
+ )
243
+ }
244
+ }