@notionhq/custom-blocks 0.1.34 → 0.1.36

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 (139) hide show
  1. package/README.md +2 -4
  2. package/dist/autoResize.d.ts.map +1 -1
  3. package/dist/bridge/SandboxBridge.d.ts +1 -0
  4. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  5. package/dist/bridge/SandboxBridge.js +256 -221
  6. package/dist/bridge/appearance.d.ts.map +1 -1
  7. package/dist/bridge/dataSources/query.d.ts +2 -1
  8. package/dist/bridge/dataSources/query.d.ts.map +1 -1
  9. package/dist/bridge/dataSources/query.js +296 -161
  10. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  11. package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -1
  12. package/dist/bridge/hostState.d.ts.map +1 -1
  13. package/dist/bridge/loadManifest.d.ts.map +1 -1
  14. package/dist/bridge/pendingRequests.d.ts.map +1 -1
  15. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  16. package/dist/customBlock.d.ts.map +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/init.d.ts.map +1 -1
  19. package/dist/init.js +1 -1
  20. package/dist/protocol/contrast.d.ts +0 -1
  21. package/dist/protocol/dataSources/dataSource.d.ts +0 -1
  22. package/dist/protocol/dataSources/dataSourcePage.d.ts +0 -1
  23. package/dist/protocol/dataSources/dataSourceValue.d.ts +0 -1
  24. package/dist/protocol/dataSources/dateValue.d.ts +0 -1
  25. package/dist/protocol/dataSources/errors.d.ts +0 -1
  26. package/dist/protocol/dataSources/propertySchema.d.ts +7 -1
  27. package/dist/protocol/dataSources/propertySchema.js +17 -0
  28. package/dist/protocol/dataSources/recordPointer.d.ts +0 -1
  29. package/dist/protocol/errors.d.ts +0 -1
  30. package/dist/protocol/ids.d.ts +0 -1
  31. package/dist/protocol/incomingType.d.ts +0 -1
  32. package/dist/protocol/index.d.ts +0 -1
  33. package/dist/protocol/manifest.d.ts +0 -1
  34. package/dist/protocol/messagePayload.d.ts +0 -1
  35. package/dist/protocol/messages/connect.d.ts +0 -1
  36. package/dist/protocol/messages/contrastModeChanged.d.ts +0 -1
  37. package/dist/protocol/messages/createPage.d.ts +0 -1
  38. package/dist/protocol/messages/createPage.js +0 -1
  39. package/dist/protocol/messages/createPageResult.d.ts +0 -1
  40. package/dist/protocol/messages/currentUserChanged.d.ts +0 -1
  41. package/dist/protocol/messages/dataSourcesChanged.d.ts +0 -1
  42. package/dist/protocol/messages/getPage.d.ts +0 -1
  43. package/dist/protocol/messages/getUser.d.ts +0 -1
  44. package/dist/protocol/messages/hostToSandbox.d.ts +3 -1
  45. package/dist/protocol/messages/init.d.ts +0 -1
  46. package/dist/protocol/messages/initResult.d.ts +0 -1
  47. package/dist/protocol/messages/invalidHostMessage.d.ts +0 -1
  48. package/dist/protocol/messages/invalidSandboxMessage.d.ts +0 -1
  49. package/dist/protocol/messages/listUsers.d.ts +0 -1
  50. package/dist/protocol/messages/pageChanged.d.ts +0 -1
  51. package/dist/protocol/messages/parentChanged.d.ts +0 -1
  52. package/dist/protocol/messages/queryDataSource.d.ts +15 -1
  53. package/dist/protocol/messages/queryDataSource.js +8 -0
  54. package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -2
  55. package/dist/protocol/messages/resize.d.ts +0 -1
  56. package/dist/protocol/messages/sandboxToHost.d.ts +4 -1
  57. package/dist/protocol/messages/themeChanged.d.ts +0 -1
  58. package/dist/protocol/messages/updatePage.d.ts +0 -1
  59. package/dist/protocol/messages/updatePageResult.d.ts +0 -1
  60. package/dist/protocol/pages/page.d.ts +0 -1
  61. package/dist/protocol/pages/page.js +0 -1
  62. package/dist/protocol/parent.d.ts +0 -1
  63. package/dist/protocol/protocolVersion.d.ts +0 -1
  64. package/dist/protocol/theme.d.ts +0 -1
  65. package/dist/protocol/users/user.d.ts +0 -1
  66. package/dist/react/DebugMessageLog.d.ts.map +1 -1
  67. package/dist/react/NotionCustomBlock.css +0 -11
  68. package/dist/react/NotionCustomBlock.d.ts.map +1 -1
  69. package/dist/react/NotionCustomBlock.js +14 -3
  70. package/dist/react/NotionTokenScope.d.ts.map +1 -1
  71. package/dist/react/index.d.ts.map +1 -1
  72. package/dist/react/standalonePreview.d.ts.map +1 -1
  73. package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -1
  74. package/dist/react/useCustomBlockInit.d.ts.map +1 -1
  75. package/dist/react/useDataSource.d.ts +1 -1
  76. package/dist/react/useDataSource.d.ts.map +1 -1
  77. package/dist/react/useDataSource.js +1 -1
  78. package/dist/react/useHostState.d.ts.map +1 -1
  79. package/dist/react/useRuntimeState.d.ts.map +1 -1
  80. package/dist/testing.d.ts +17 -0
  81. package/dist/testing.d.ts.map +1 -0
  82. package/dist/testing.js +13 -0
  83. package/dist/types.d.ts +7 -0
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/utils.d.ts.map +1 -1
  86. package/dist/version.d.ts.map +1 -1
  87. package/dist/version.js +1 -1
  88. package/docs/data-sources.md +13 -1
  89. package/package.json +5 -6
  90. package/src/bridge/SandboxBridge.ts +49 -5
  91. package/src/bridge/dataSources/query.ts +394 -170
  92. package/src/init.ts +2 -1
  93. package/src/react/NotionCustomBlock.css +0 -11
  94. package/src/react/NotionCustomBlock.tsx +28 -10
  95. package/src/react/useDataSource.ts +1 -1
  96. package/src/testing.ts +26 -0
  97. package/src/types.ts +8 -1
  98. package/dist/protocol/contrast.d.ts.map +0 -1
  99. package/dist/protocol/dataSources/dataSource.d.ts.map +0 -1
  100. package/dist/protocol/dataSources/dataSourcePage.d.ts.map +0 -1
  101. package/dist/protocol/dataSources/dataSourceValue.d.ts.map +0 -1
  102. package/dist/protocol/dataSources/dateValue.d.ts.map +0 -1
  103. package/dist/protocol/dataSources/errors.d.ts.map +0 -1
  104. package/dist/protocol/dataSources/propertySchema.d.ts.map +0 -1
  105. package/dist/protocol/dataSources/recordPointer.d.ts.map +0 -1
  106. package/dist/protocol/errors.d.ts.map +0 -1
  107. package/dist/protocol/ids.d.ts.map +0 -1
  108. package/dist/protocol/incomingType.d.ts.map +0 -1
  109. package/dist/protocol/index.d.ts.map +0 -1
  110. package/dist/protocol/manifest.d.ts.map +0 -1
  111. package/dist/protocol/messagePayload.d.ts.map +0 -1
  112. package/dist/protocol/messages/connect.d.ts.map +0 -1
  113. package/dist/protocol/messages/contrastModeChanged.d.ts.map +0 -1
  114. package/dist/protocol/messages/createPage.d.ts.map +0 -1
  115. package/dist/protocol/messages/createPageResult.d.ts.map +0 -1
  116. package/dist/protocol/messages/currentUserChanged.d.ts.map +0 -1
  117. package/dist/protocol/messages/dataSourcesChanged.d.ts.map +0 -1
  118. package/dist/protocol/messages/getPage.d.ts.map +0 -1
  119. package/dist/protocol/messages/getUser.d.ts.map +0 -1
  120. package/dist/protocol/messages/hostToSandbox.d.ts.map +0 -1
  121. package/dist/protocol/messages/init.d.ts.map +0 -1
  122. package/dist/protocol/messages/initResult.d.ts.map +0 -1
  123. package/dist/protocol/messages/invalidHostMessage.d.ts.map +0 -1
  124. package/dist/protocol/messages/invalidSandboxMessage.d.ts.map +0 -1
  125. package/dist/protocol/messages/listUsers.d.ts.map +0 -1
  126. package/dist/protocol/messages/pageChanged.d.ts.map +0 -1
  127. package/dist/protocol/messages/parentChanged.d.ts.map +0 -1
  128. package/dist/protocol/messages/queryDataSource.d.ts.map +0 -1
  129. package/dist/protocol/messages/queryDataSourceResult.d.ts.map +0 -1
  130. package/dist/protocol/messages/resize.d.ts.map +0 -1
  131. package/dist/protocol/messages/sandboxToHost.d.ts.map +0 -1
  132. package/dist/protocol/messages/themeChanged.d.ts.map +0 -1
  133. package/dist/protocol/messages/updatePage.d.ts.map +0 -1
  134. package/dist/protocol/messages/updatePageResult.d.ts.map +0 -1
  135. package/dist/protocol/pages/page.d.ts.map +0 -1
  136. package/dist/protocol/parent.d.ts.map +0 -1
  137. package/dist/protocol/protocolVersion.d.ts.map +0 -1
  138. package/dist/protocol/theme.d.ts.map +0 -1
  139. package/dist/protocol/users/user.d.ts.map +0 -1
@@ -1,6 +1,8 @@
1
1
  import type { NotionDataSource } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
2
+ import { CUSTOM_BLOCK_DATA_SOURCE_SORTABLE_PROPERTY_TYPES } from "@notionhq/custom-blocks-protocol/dataSources/propertySchema.js"
2
3
  import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
3
4
  import {
5
+ CUSTOM_BLOCK_DATA_SOURCE_QUERY_MAX_SORTS,
4
6
  customBlockCheckboxFilterOperatorSchema,
5
7
  customBlockContainsFilterOperatorSchema,
6
8
  customBlockDateFilterOperatorSchema,
@@ -9,18 +11,24 @@ import {
9
11
  customBlockTextFilterOperatorSchema,
10
12
  type ResolvedCustomBlockDataSourceFilter,
11
13
  type ResolvedCustomBlockDataSourcePropertyFilter,
14
+ type ResolvedCustomBlockDataSourceSort,
12
15
  } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
13
16
  import * as v from "valibot"
14
17
  import type { UseDataSourceOptions } from "../../types.js"
18
+ import { unreachable } from "../../utils.js"
15
19
 
16
20
  const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
17
21
  const MAX_DATA_SOURCE_QUERY_LIMIT = 999
18
22
  const MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN = 25
23
+ const supportedDataSourceSortPropertyTypes = new Set<string>(
24
+ CUSTOM_BLOCK_DATA_SOURCE_SORTABLE_PROPERTY_TYPES,
25
+ )
19
26
 
20
27
  export type ResolvedDataSourceQuery = {
21
28
  dataSourceId: NotionDataSourceId
22
29
  limit: number
23
30
  filter?: ResolvedCustomBlockDataSourceFilter
31
+ sorts?: ResolvedCustomBlockDataSourceSort[]
24
32
  identity: string
25
33
  }
26
34
 
@@ -32,6 +40,16 @@ export type ResolveDataSourceQueryResult =
32
40
  query: ResolvedDataSourceQuery
33
41
  }
34
42
 
43
+ type DataSourceQueryOptions = {
44
+ limit?: unknown
45
+ filter?: unknown
46
+ sorts?: unknown
47
+ }
48
+
49
+ type ResolvePropertyFilterResult =
50
+ | { status: "ok"; filter: ResolvedCustomBlockDataSourcePropertyFilter }
51
+ | { status: "error"; error: string }
52
+
35
53
  export function resolveDataSourceQuery(args: {
36
54
  dataSources: NotionDataSource[]
37
55
  key: string
@@ -39,21 +57,11 @@ export function resolveDataSourceQuery(args: {
39
57
  warn?: (message: string) => void
40
58
  }): ResolveDataSourceQueryResult {
41
59
  const { dataSources, key, options, warn } = args
42
- if (options !== undefined && !isPlainRecord(options)) {
43
- return {
44
- status: "error",
45
- error: "Data source query options must be an object.",
46
- }
47
- }
48
- if (
49
- options !== undefined &&
50
- Object.keys(options).some(key => !["limit", "filter"].includes(key))
51
- ) {
52
- return {
53
- status: "error",
54
- error: "Data source query options contain unsupported fields.",
55
- }
60
+ const resolvedOptions = resolveDataSourceQueryOptions(options)
61
+ if (resolvedOptions.status === "error") {
62
+ return resolvedOptions
56
63
  }
64
+ const queryOptions = resolvedOptions.options
57
65
  const dataSource = dataSources.find(entry => entry.key === key)
58
66
  if (dataSource === undefined) {
59
67
  return {
@@ -68,19 +76,24 @@ export function resolveDataSourceQuery(args: {
68
76
  }
69
77
  }
70
78
 
71
- const limit = resolveDataSourceQueryLimit(options?.limit, warn)
79
+ const limit = resolveDataSourceQueryLimit(queryOptions?.limit, warn)
72
80
  if (limit.status === "error") {
73
81
  return limit
74
82
  }
75
- const filter = resolveFilter(options?.filter, dataSource)
83
+ const filter = resolveFilter(queryOptions?.filter, dataSource)
76
84
  if (filter.status === "error") {
77
85
  return filter
78
86
  }
87
+ const sorts = resolveSorts(queryOptions?.sorts, dataSource)
88
+ if (sorts.status === "error") {
89
+ return sorts
90
+ }
79
91
 
80
92
  const identity = stableJsonStringify({
81
93
  dataSourceId: dataSource.collectionPointer.id,
82
94
  limit: limit.limit,
83
95
  filter: filter.filter ?? null,
96
+ sorts: sorts.sorts ?? [],
84
97
  })
85
98
  return {
86
99
  status: "ok",
@@ -89,11 +102,64 @@ export function resolveDataSourceQuery(args: {
89
102
  dataSourceId: dataSource.collectionPointer.id,
90
103
  limit: limit.limit,
91
104
  filter: filter.filter,
105
+ sorts: sorts.sorts,
92
106
  identity,
93
107
  },
94
108
  }
95
109
  }
96
110
 
111
+ function resolveDataSourceQueryOptions(
112
+ options: unknown,
113
+ ):
114
+ | { status: "ok"; options?: DataSourceQueryOptions }
115
+ | { status: "error"; error: string } {
116
+ if (options === undefined) {
117
+ return { status: "ok" }
118
+ }
119
+ if (
120
+ typeof options !== "object" ||
121
+ options === null ||
122
+ Array.isArray(options)
123
+ ) {
124
+ return {
125
+ status: "error",
126
+ error: "Data source query options must be an object.",
127
+ }
128
+ }
129
+ const prototype = Object.getPrototypeOf(options)
130
+ if (prototype !== Object.prototype && prototype !== null) {
131
+ return {
132
+ status: "error",
133
+ error: "Data source query options must be an object.",
134
+ }
135
+ }
136
+ const optionsObject = options as Record<string, unknown>
137
+ if (
138
+ Object.keys(optionsObject).some(
139
+ key => !["limit", "filter", "sorts"].includes(key),
140
+ )
141
+ ) {
142
+ return {
143
+ status: "error",
144
+ error: "Data source query options contain unsupported fields.",
145
+ }
146
+ }
147
+ return {
148
+ status: "ok",
149
+ options: {
150
+ limit: Object.prototype.hasOwnProperty.call(optionsObject, "limit")
151
+ ? optionsObject.limit
152
+ : undefined,
153
+ filter: Object.prototype.hasOwnProperty.call(optionsObject, "filter")
154
+ ? optionsObject.filter
155
+ : undefined,
156
+ sorts: Object.prototype.hasOwnProperty.call(optionsObject, "sorts")
157
+ ? optionsObject.sorts
158
+ : undefined,
159
+ },
160
+ }
161
+ }
162
+
97
163
  function resolveDataSourceQueryLimit(
98
164
  limit: unknown,
99
165
  warn: ((message: string) => void) | undefined = console.warn,
@@ -144,27 +210,31 @@ function resolveFilter(
144
210
  if (filter === undefined) {
145
211
  return { status: "ok" }
146
212
  }
147
- if (!isPlainRecord(filter)) {
213
+ if (typeof filter !== "object" || filter === null || Array.isArray(filter)) {
148
214
  return {
149
215
  status: "error",
150
216
  error: "Data source query filter must be an object.",
151
217
  }
152
218
  }
153
- if ("and" in filter) {
154
- if (Object.keys(filter).length !== 1 || !Array.isArray(filter.and)) {
219
+ const filterObject = filter as Record<string, unknown>
220
+ if (Object.prototype.hasOwnProperty.call(filterObject, "and")) {
221
+ if (
222
+ Object.keys(filterObject).length !== 1 ||
223
+ !Array.isArray(filterObject.and)
224
+ ) {
155
225
  return {
156
226
  status: "error",
157
227
  error: 'Data source query filter "and" must be an array.',
158
228
  }
159
229
  }
160
- if (filter.and.length > MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN) {
230
+ if (filterObject.and.length > MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN) {
161
231
  return {
162
232
  status: "error",
163
233
  error: `Data source query filter "and" may contain at most ${MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN} children.`,
164
234
  }
165
235
  }
166
236
  const and: ResolvedCustomBlockDataSourcePropertyFilter[] = []
167
- for (const child of filter.and) {
237
+ for (const child of filterObject.and) {
168
238
  const resolved = resolvePropertyFilter(child, dataSource)
169
239
  if (resolved.status === "error") {
170
240
  return resolved
@@ -173,26 +243,119 @@ function resolveFilter(
173
243
  }
174
244
  return { status: "ok", filter: { and } }
175
245
  }
176
- return resolvePropertyFilter(filter, dataSource)
246
+ return resolvePropertyFilter(filterObject, dataSource)
177
247
  }
178
248
 
179
- function resolvePropertyFilter(
180
- filter: unknown,
249
+ function resolveSorts(
250
+ sorts: unknown,
181
251
  dataSource: NotionDataSource,
182
252
  ):
183
- | { status: "ok"; filter: ResolvedCustomBlockDataSourcePropertyFilter }
253
+ | { status: "ok"; sorts?: ResolvedCustomBlockDataSourceSort[] }
254
+ | { status: "error"; error: string } {
255
+ if (sorts === undefined) {
256
+ return { status: "ok" }
257
+ }
258
+ if (!Array.isArray(sorts)) {
259
+ return {
260
+ status: "error",
261
+ error: "Data source query sorts must be an array.",
262
+ }
263
+ }
264
+ if (sorts.length === 0) {
265
+ return { status: "ok" }
266
+ }
267
+ if (sorts.length > CUSTOM_BLOCK_DATA_SOURCE_QUERY_MAX_SORTS) {
268
+ return {
269
+ status: "error",
270
+ error: `Data source query sorts may contain at most ${CUSTOM_BLOCK_DATA_SOURCE_QUERY_MAX_SORTS} entries.`,
271
+ }
272
+ }
273
+
274
+ const resolvedSorts: ResolvedCustomBlockDataSourceSort[] = []
275
+ const seenPropertyIds = new Set<string>()
276
+ for (const sort of sorts) {
277
+ const resolved = resolveSort(sort, dataSource)
278
+ if (resolved.status === "error") {
279
+ return resolved
280
+ }
281
+ if (seenPropertyIds.has(resolved.sort.propertyId)) {
282
+ return {
283
+ status: "error",
284
+ error: `Data source query sorts cannot contain duplicate property ID "${resolved.sort.propertyId}".`,
285
+ }
286
+ }
287
+ seenPropertyIds.add(resolved.sort.propertyId)
288
+ resolvedSorts.push(resolved.sort)
289
+ }
290
+ return { status: "ok", sorts: resolvedSorts }
291
+ }
292
+
293
+ function resolveSort(
294
+ sort: unknown,
295
+ dataSource: NotionDataSource,
296
+ ):
297
+ | { status: "ok"; sort: ResolvedCustomBlockDataSourceSort }
184
298
  | { status: "error"; error: string } {
185
- if (!isPlainRecord(filter)) {
299
+ if (typeof sort !== "object" || sort === null || Array.isArray(sort)) {
300
+ return {
301
+ status: "error",
302
+ error: "Data source query sort must be an object.",
303
+ }
304
+ }
305
+ const sortObject = sort as Record<string, unknown>
306
+ if (
307
+ Object.keys(sortObject).some(
308
+ key => !["key", "propertyId", "direction"].includes(key),
309
+ )
310
+ ) {
311
+ return {
312
+ status: "error",
313
+ error: "Data source query sort contains unsupported fields.",
314
+ }
315
+ }
316
+ const property = resolvePropertyAddress(sortObject, dataSource)
317
+ if (property.status === "error") {
318
+ return property
319
+ }
320
+ if (!supportedDataSourceSortPropertyTypes.has(property.propertyType)) {
321
+ return {
322
+ status: "error",
323
+ error: `Data source query sorts do not yet support property type "${property.propertyType}".`,
324
+ }
325
+ }
326
+ const direction = sortObject.direction
327
+ if (
328
+ !Object.prototype.hasOwnProperty.call(sortObject, "direction") ||
329
+ (direction !== "ascending" && direction !== "descending")
330
+ ) {
331
+ return {
332
+ status: "error",
333
+ error:
334
+ 'Data source query sort direction must be "ascending" or "descending".',
335
+ }
336
+ }
337
+ return {
338
+ status: "ok",
339
+ sort: { propertyId: property.propertyId, direction },
340
+ }
341
+ }
342
+
343
+ function resolvePropertyFilter(
344
+ filter: unknown,
345
+ dataSource: NotionDataSource,
346
+ ): ResolvePropertyFilterResult {
347
+ if (typeof filter !== "object" || filter === null || Array.isArray(filter)) {
186
348
  return {
187
349
  status: "error",
188
350
  error: "Data source query property filter must be an object.",
189
351
  }
190
352
  }
191
- const property = resolvePropertyAddress(filter, dataSource)
353
+ const filterObject = filter as Record<string, unknown>
354
+ const property = resolvePropertyAddress(filterObject, dataSource)
192
355
  if (property.status === "error") {
193
356
  return property
194
357
  }
195
- const branchKeys = Object.keys(filter).filter(
358
+ const branchKeys = Object.keys(filterObject).filter(
196
359
  key => key !== "key" && key !== "propertyId",
197
360
  )
198
361
  if (branchKeys.length !== 1) {
@@ -203,8 +366,13 @@ function resolvePropertyFilter(
203
366
  }
204
367
  }
205
368
  const branch = branchKeys[0]
206
- const value = filter[branch]
207
- if (!isPlainRecord(value) || Object.keys(value).length !== 1) {
369
+ const value = filterObject[branch]
370
+ if (
371
+ typeof value !== "object" ||
372
+ value === null ||
373
+ Array.isArray(value) ||
374
+ Object.keys(value).length !== 1
375
+ ) {
208
376
  return invalidOperator(branch)
209
377
  }
210
378
  if (property.propertyType !== branch) {
@@ -214,132 +382,172 @@ function resolvePropertyFilter(
214
382
  }
215
383
  }
216
384
 
217
- switch (branch) {
218
- case "title": {
219
- const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
220
- return parsed.success
221
- ? {
222
- status: "ok",
223
- filter: { propertyId: property.propertyId, title: parsed.output },
224
- }
225
- : invalidOperator(branch)
226
- }
227
- case "rich_text": {
228
- const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
229
- return parsed.success
230
- ? {
231
- status: "ok",
232
- filter: {
233
- propertyId: property.propertyId,
234
- rich_text: parsed.output,
235
- },
236
- }
237
- : invalidOperator(branch)
238
- }
385
+ return resolvePropertyFilterBranch({
386
+ propertyId: property.propertyId,
387
+ branch,
388
+ value: value as Record<string, unknown>,
389
+ })
390
+ }
391
+
392
+ function resolvePropertyFilterBranch(args: {
393
+ propertyId: string
394
+ branch: string
395
+ value: Record<string, unknown>
396
+ }): ResolvePropertyFilterResult {
397
+ switch (args.branch) {
398
+ case "title":
399
+ case "rich_text":
239
400
  case "url":
240
401
  case "email":
241
- case "phone_number": {
242
- const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
243
- if (!parsed.success) {
244
- return invalidOperator(branch)
402
+ case "phone_number":
403
+ return resolveTextPropertyFilter({
404
+ propertyId: args.propertyId,
405
+ branch: args.branch,
406
+ value: args.value,
407
+ })
408
+ case "number":
409
+ return resolveNumberPropertyFilter(args.propertyId, args.value)
410
+ case "checkbox":
411
+ return resolveCheckboxPropertyFilter(args.propertyId, args.value)
412
+ case "select":
413
+ case "status":
414
+ return resolveOptionPropertyFilter({
415
+ propertyId: args.propertyId,
416
+ branch: args.branch,
417
+ value: args.value,
418
+ })
419
+ case "multi_select":
420
+ return resolveMultiSelectPropertyFilter(args.propertyId, args.value)
421
+ case "date":
422
+ return resolveDatePropertyFilter(args.propertyId, args.value)
423
+ default:
424
+ return {
425
+ status: "error",
426
+ error: `Data source query filter branch "${args.branch}" is not supported.`,
245
427
  }
246
- if (branch === "url") {
247
- return {
248
- status: "ok",
249
- filter: { propertyId: property.propertyId, url: parsed.output },
250
- }
428
+ }
429
+ }
430
+
431
+ function resolveTextPropertyFilter(args: {
432
+ propertyId: string
433
+ branch: "title" | "rich_text" | "url" | "email" | "phone_number"
434
+ value: Record<string, unknown>
435
+ }): ResolvePropertyFilterResult {
436
+ const parsed = v.safeParse(customBlockTextFilterOperatorSchema, args.value)
437
+ if (!parsed.success) {
438
+ return invalidOperator(args.branch)
439
+ }
440
+ switch (args.branch) {
441
+ case "title":
442
+ return {
443
+ status: "ok",
444
+ filter: { propertyId: args.propertyId, title: parsed.output },
251
445
  }
252
- if (branch === "email") {
253
- return {
254
- status: "ok",
255
- filter: { propertyId: property.propertyId, email: parsed.output },
256
- }
446
+ case "rich_text":
447
+ return {
448
+ status: "ok",
449
+ filter: { propertyId: args.propertyId, rich_text: parsed.output },
257
450
  }
451
+ case "url":
258
452
  return {
259
453
  status: "ok",
260
- filter: {
261
- propertyId: property.propertyId,
262
- phone_number: parsed.output,
263
- },
454
+ filter: { propertyId: args.propertyId, url: parsed.output },
264
455
  }
265
- }
266
- case "number": {
267
- const parsed = v.safeParse(customBlockNumberFilterOperatorSchema, value)
268
- if (
269
- parsed.success &&
270
- Object.values(parsed.output).every(
271
- entry => typeof entry !== "number" || Number.isFinite(entry),
272
- )
273
- ) {
274
- return {
275
- status: "ok",
276
- filter: { propertyId: property.propertyId, number: parsed.output },
277
- }
456
+ case "email":
457
+ return {
458
+ status: "ok",
459
+ filter: { propertyId: args.propertyId, email: parsed.output },
278
460
  }
279
- return invalidOperator(branch)
280
- }
281
- case "checkbox": {
282
- const parsed = v.safeParse(customBlockCheckboxFilterOperatorSchema, value)
283
- return parsed.success
284
- ? {
285
- status: "ok",
286
- filter: {
287
- propertyId: property.propertyId,
288
- checkbox: parsed.output,
289
- },
290
- }
291
- : invalidOperator(branch)
292
- }
293
- case "select": {
294
- const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, value)
295
- return parsed.success
296
- ? {
297
- status: "ok",
298
- filter: { propertyId: property.propertyId, select: parsed.output },
299
- }
300
- : invalidOperator(branch)
301
- }
302
- case "multi_select": {
303
- const parsed = v.safeParse(customBlockContainsFilterOperatorSchema, value)
304
- return parsed.success
305
- ? {
306
- status: "ok",
307
- filter: {
308
- propertyId: property.propertyId,
309
- multi_select: parsed.output,
310
- },
311
- }
312
- : invalidOperator(branch)
313
- }
314
- case "status": {
315
- const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, value)
316
- return parsed.success
317
- ? {
318
- status: "ok",
319
- filter: { propertyId: property.propertyId, status: parsed.output },
320
- }
321
- : invalidOperator(branch)
322
- }
323
- case "date": {
324
- const parsed = v.safeParse(customBlockDateFilterOperatorSchema, value)
325
- if (
326
- parsed.success &&
327
- Object.values(parsed.output).every(
328
- entry => entry === true || isValidIsoDate(entry),
329
- )
330
- ) {
331
- return {
332
- status: "ok",
333
- filter: { propertyId: property.propertyId, date: parsed.output },
334
- }
461
+ case "phone_number":
462
+ return {
463
+ status: "ok",
464
+ filter: { propertyId: args.propertyId, phone_number: parsed.output },
335
465
  }
336
- return invalidOperator(branch)
337
- }
338
466
  default:
467
+ return unreachable(args.branch)
468
+ }
469
+ }
470
+
471
+ function resolveNumberPropertyFilter(
472
+ propertyId: string,
473
+ value: Record<string, unknown>,
474
+ ): ResolvePropertyFilterResult {
475
+ const parsed = v.safeParse(customBlockNumberFilterOperatorSchema, value)
476
+ if (
477
+ !parsed.success ||
478
+ !Object.values(parsed.output).every(
479
+ entry => typeof entry !== "number" || Number.isFinite(entry),
480
+ )
481
+ ) {
482
+ return invalidOperator("number")
483
+ }
484
+ return {
485
+ status: "ok",
486
+ filter: { propertyId, number: parsed.output },
487
+ }
488
+ }
489
+
490
+ function resolveCheckboxPropertyFilter(
491
+ propertyId: string,
492
+ value: Record<string, unknown>,
493
+ ): ResolvePropertyFilterResult {
494
+ const parsed = v.safeParse(customBlockCheckboxFilterOperatorSchema, value)
495
+ return parsed.success
496
+ ? { status: "ok", filter: { propertyId, checkbox: parsed.output } }
497
+ : invalidOperator("checkbox")
498
+ }
499
+
500
+ function resolveOptionPropertyFilter(args: {
501
+ propertyId: string
502
+ branch: "select" | "status"
503
+ value: Record<string, unknown>
504
+ }): ResolvePropertyFilterResult {
505
+ const parsed = v.safeParse(customBlockOptionFilterOperatorSchema, args.value)
506
+ if (!parsed.success) {
507
+ return invalidOperator(args.branch)
508
+ }
509
+ switch (args.branch) {
510
+ case "select":
339
511
  return {
340
- status: "error",
341
- error: `Data source query filter branch "${branch}" is not supported.`,
512
+ status: "ok",
513
+ filter: { propertyId: args.propertyId, select: parsed.output },
514
+ }
515
+ case "status":
516
+ return {
517
+ status: "ok",
518
+ filter: { propertyId: args.propertyId, status: parsed.output },
342
519
  }
520
+ default:
521
+ return unreachable(args.branch)
522
+ }
523
+ }
524
+
525
+ function resolveMultiSelectPropertyFilter(
526
+ propertyId: string,
527
+ value: Record<string, unknown>,
528
+ ): ResolvePropertyFilterResult {
529
+ const parsed = v.safeParse(customBlockContainsFilterOperatorSchema, value)
530
+ return parsed.success
531
+ ? { status: "ok", filter: { propertyId, multi_select: parsed.output } }
532
+ : invalidOperator("multi_select")
533
+ }
534
+
535
+ function resolveDatePropertyFilter(
536
+ propertyId: string,
537
+ value: Record<string, unknown>,
538
+ ): ResolvePropertyFilterResult {
539
+ const parsed = v.safeParse(customBlockDateFilterOperatorSchema, value)
540
+ if (
541
+ !parsed.success ||
542
+ !Object.values(parsed.output).every(
543
+ entry => entry === true || isValidIsoDate(entry),
544
+ )
545
+ ) {
546
+ return invalidOperator("date")
547
+ }
548
+ return {
549
+ status: "ok",
550
+ filter: { propertyId, date: parsed.output },
343
551
  }
344
552
  }
345
553
 
@@ -349,13 +557,16 @@ function resolvePropertyAddress(
349
557
  ):
350
558
  | { status: "ok"; propertyId: string; propertyType: string }
351
559
  | { status: "error"; error: string } {
352
- const hasKey = "key" in value
353
- const hasPropertyId = "propertyId" in value
560
+ const hasKey = Object.prototype.hasOwnProperty.call(value, "key")
561
+ const hasPropertyId = Object.prototype.hasOwnProperty.call(
562
+ value,
563
+ "propertyId",
564
+ )
354
565
  if (hasKey === hasPropertyId) {
355
566
  return {
356
567
  status: "error",
357
568
  error:
358
- "Data source query filters must use exactly one of key or propertyId.",
569
+ "Data source query filters and sorts must use exactly one of key or propertyId.",
359
570
  }
360
571
  }
361
572
  let propertyId: string
@@ -366,7 +577,12 @@ function resolvePropertyAddress(
366
577
  error: "Data source query property key must be a string.",
367
578
  }
368
579
  }
369
- if (!(value.key in dataSource.propertyIdsByKey)) {
580
+ if (
581
+ !Object.prototype.hasOwnProperty.call(
582
+ dataSource.propertyIdsByKey,
583
+ value.key,
584
+ )
585
+ ) {
370
586
  return {
371
587
  status: "error",
372
588
  error: `Unknown property key "${value.key}" for data source "${dataSource.key}".`,
@@ -389,13 +605,18 @@ function resolvePropertyAddress(
389
605
  }
390
606
  propertyId = value.propertyId
391
607
  }
392
- const propertySchema = dataSource.propertySchemasById[propertyId]
393
- if (propertySchema === undefined) {
608
+ if (
609
+ !Object.prototype.hasOwnProperty.call(
610
+ dataSource.propertySchemasById,
611
+ propertyId,
612
+ )
613
+ ) {
394
614
  return {
395
615
  status: "error",
396
616
  error: `Unknown property ID "${propertyId}" for data source "${dataSource.key}".`,
397
617
  }
398
618
  }
619
+ const propertySchema = dataSource.propertySchemasById[propertyId]
399
620
  return {
400
621
  status: "ok",
401
622
  propertyId,
@@ -419,35 +640,38 @@ function normalizeJsonValue(value: unknown): unknown {
419
640
  return value
420
641
  }
421
642
  if (typeof value === "number") {
422
- if (!Number.isFinite(value)) {
423
- throw new Error("Data source query values must be finite JSON numbers.")
424
- }
425
- return value
643
+ return normalizeJsonNumber(value)
426
644
  }
427
645
  if (Array.isArray(value)) {
428
646
  return value.map(entry => normalizeJsonValue(entry))
429
647
  }
430
- if (isPlainRecord(value)) {
431
- const normalized: Record<string, unknown> = {}
432
- for (const key of Object.keys(value).sort()) {
433
- const child = value[key]
434
- if (child !== undefined) {
435
- normalized[key] = normalizeJsonValue(child)
436
- }
437
- }
438
- return normalized
648
+ if (typeof value === "object") {
649
+ return normalizeJsonObject(value)
439
650
  }
440
651
  throw new Error("Data source query values must be JSON-compatible.")
441
652
  }
442
653
 
443
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
444
- return (
445
- typeof value === "object" &&
446
- value !== null &&
447
- !Array.isArray(value) &&
448
- (Object.getPrototypeOf(value) === Object.prototype ||
449
- Object.getPrototypeOf(value) === null)
450
- )
654
+ function normalizeJsonNumber(value: number): number {
655
+ if (!Number.isFinite(value)) {
656
+ throw new Error("Data source query values must be finite JSON numbers.")
657
+ }
658
+ return value
659
+ }
660
+
661
+ function normalizeJsonObject(value: object): Record<string, unknown> {
662
+ const prototype = Object.getPrototypeOf(value)
663
+ if (prototype !== Object.prototype && prototype !== null) {
664
+ throw new Error("Data source query values must be JSON-compatible.")
665
+ }
666
+ const objectValue = value as Record<string, unknown>
667
+ const normalized: Record<string, unknown> = {}
668
+ for (const key of Object.keys(objectValue).sort()) {
669
+ const child = objectValue[key]
670
+ if (child !== undefined) {
671
+ normalized[key] = normalizeJsonValue(child)
672
+ }
673
+ }
674
+ return normalized
451
675
  }
452
676
 
453
677
  function isValidIsoDate(value: unknown): boolean {