@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.
- package/README.md +2 -4
- package/dist/autoResize.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.d.ts +1 -0
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +256 -221
- package/dist/bridge/appearance.d.ts.map +1 -1
- package/dist/bridge/dataSources/query.d.ts +2 -1
- package/dist/bridge/dataSources/query.d.ts.map +1 -1
- package/dist/bridge/dataSources/query.js +296 -161
- package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
- package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -1
- package/dist/bridge/hostState.d.ts.map +1 -1
- package/dist/bridge/loadManifest.d.ts.map +1 -1
- package/dist/bridge/pendingRequests.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/customBlock.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +1 -1
- package/dist/protocol/contrast.d.ts +0 -1
- package/dist/protocol/dataSources/dataSource.d.ts +0 -1
- package/dist/protocol/dataSources/dataSourcePage.d.ts +0 -1
- package/dist/protocol/dataSources/dataSourceValue.d.ts +0 -1
- package/dist/protocol/dataSources/dateValue.d.ts +0 -1
- package/dist/protocol/dataSources/errors.d.ts +0 -1
- package/dist/protocol/dataSources/propertySchema.d.ts +7 -1
- package/dist/protocol/dataSources/propertySchema.js +17 -0
- package/dist/protocol/dataSources/recordPointer.d.ts +0 -1
- package/dist/protocol/errors.d.ts +0 -1
- package/dist/protocol/ids.d.ts +0 -1
- package/dist/protocol/incomingType.d.ts +0 -1
- package/dist/protocol/index.d.ts +0 -1
- package/dist/protocol/manifest.d.ts +0 -1
- package/dist/protocol/messagePayload.d.ts +0 -1
- package/dist/protocol/messages/connect.d.ts +0 -1
- package/dist/protocol/messages/contrastModeChanged.d.ts +0 -1
- package/dist/protocol/messages/createPage.d.ts +0 -1
- package/dist/protocol/messages/createPage.js +0 -1
- package/dist/protocol/messages/createPageResult.d.ts +0 -1
- package/dist/protocol/messages/currentUserChanged.d.ts +0 -1
- package/dist/protocol/messages/dataSourcesChanged.d.ts +0 -1
- package/dist/protocol/messages/getPage.d.ts +0 -1
- package/dist/protocol/messages/getUser.d.ts +0 -1
- package/dist/protocol/messages/hostToSandbox.d.ts +3 -1
- package/dist/protocol/messages/init.d.ts +0 -1
- package/dist/protocol/messages/initResult.d.ts +0 -1
- package/dist/protocol/messages/invalidHostMessage.d.ts +0 -1
- package/dist/protocol/messages/invalidSandboxMessage.d.ts +0 -1
- package/dist/protocol/messages/listUsers.d.ts +0 -1
- package/dist/protocol/messages/pageChanged.d.ts +0 -1
- package/dist/protocol/messages/parentChanged.d.ts +0 -1
- package/dist/protocol/messages/queryDataSource.d.ts +15 -1
- package/dist/protocol/messages/queryDataSource.js +8 -0
- package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -2
- package/dist/protocol/messages/resize.d.ts +0 -1
- package/dist/protocol/messages/sandboxToHost.d.ts +4 -1
- package/dist/protocol/messages/themeChanged.d.ts +0 -1
- package/dist/protocol/messages/updatePage.d.ts +0 -1
- package/dist/protocol/messages/updatePageResult.d.ts +0 -1
- package/dist/protocol/pages/page.d.ts +0 -1
- package/dist/protocol/pages/page.js +0 -1
- package/dist/protocol/parent.d.ts +0 -1
- package/dist/protocol/protocolVersion.d.ts +0 -1
- package/dist/protocol/theme.d.ts +0 -1
- package/dist/protocol/users/user.d.ts +0 -1
- package/dist/react/DebugMessageLog.d.ts.map +1 -1
- package/dist/react/NotionCustomBlock.css +0 -11
- package/dist/react/NotionCustomBlock.d.ts.map +1 -1
- package/dist/react/NotionCustomBlock.js +14 -3
- package/dist/react/NotionTokenScope.d.ts.map +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/standalonePreview.d.ts.map +1 -1
- package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -1
- package/dist/react/useCustomBlockInit.d.ts.map +1 -1
- package/dist/react/useDataSource.d.ts +1 -1
- package/dist/react/useDataSource.d.ts.map +1 -1
- package/dist/react/useDataSource.js +1 -1
- package/dist/react/useHostState.d.ts.map +1 -1
- package/dist/react/useRuntimeState.d.ts.map +1 -1
- package/dist/testing.d.ts +17 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +13 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/data-sources.md +13 -1
- package/package.json +5 -6
- package/src/bridge/SandboxBridge.ts +49 -5
- package/src/bridge/dataSources/query.ts +394 -170
- package/src/init.ts +2 -1
- package/src/react/NotionCustomBlock.css +0 -11
- package/src/react/NotionCustomBlock.tsx +28 -10
- package/src/react/useDataSource.ts +1 -1
- package/src/testing.ts +26 -0
- package/src/types.ts +8 -1
- package/dist/protocol/contrast.d.ts.map +0 -1
- package/dist/protocol/dataSources/dataSource.d.ts.map +0 -1
- package/dist/protocol/dataSources/dataSourcePage.d.ts.map +0 -1
- package/dist/protocol/dataSources/dataSourceValue.d.ts.map +0 -1
- package/dist/protocol/dataSources/dateValue.d.ts.map +0 -1
- package/dist/protocol/dataSources/errors.d.ts.map +0 -1
- package/dist/protocol/dataSources/propertySchema.d.ts.map +0 -1
- package/dist/protocol/dataSources/recordPointer.d.ts.map +0 -1
- package/dist/protocol/errors.d.ts.map +0 -1
- package/dist/protocol/ids.d.ts.map +0 -1
- package/dist/protocol/incomingType.d.ts.map +0 -1
- package/dist/protocol/index.d.ts.map +0 -1
- package/dist/protocol/manifest.d.ts.map +0 -1
- package/dist/protocol/messagePayload.d.ts.map +0 -1
- package/dist/protocol/messages/connect.d.ts.map +0 -1
- package/dist/protocol/messages/contrastModeChanged.d.ts.map +0 -1
- package/dist/protocol/messages/createPage.d.ts.map +0 -1
- package/dist/protocol/messages/createPageResult.d.ts.map +0 -1
- package/dist/protocol/messages/currentUserChanged.d.ts.map +0 -1
- package/dist/protocol/messages/dataSourcesChanged.d.ts.map +0 -1
- package/dist/protocol/messages/getPage.d.ts.map +0 -1
- package/dist/protocol/messages/getUser.d.ts.map +0 -1
- package/dist/protocol/messages/hostToSandbox.d.ts.map +0 -1
- package/dist/protocol/messages/init.d.ts.map +0 -1
- package/dist/protocol/messages/initResult.d.ts.map +0 -1
- package/dist/protocol/messages/invalidHostMessage.d.ts.map +0 -1
- package/dist/protocol/messages/invalidSandboxMessage.d.ts.map +0 -1
- package/dist/protocol/messages/listUsers.d.ts.map +0 -1
- package/dist/protocol/messages/pageChanged.d.ts.map +0 -1
- package/dist/protocol/messages/parentChanged.d.ts.map +0 -1
- package/dist/protocol/messages/queryDataSource.d.ts.map +0 -1
- package/dist/protocol/messages/queryDataSourceResult.d.ts.map +0 -1
- package/dist/protocol/messages/resize.d.ts.map +0 -1
- package/dist/protocol/messages/sandboxToHost.d.ts.map +0 -1
- package/dist/protocol/messages/themeChanged.d.ts.map +0 -1
- package/dist/protocol/messages/updatePage.d.ts.map +0 -1
- package/dist/protocol/messages/updatePageResult.d.ts.map +0 -1
- package/dist/protocol/pages/page.d.ts.map +0 -1
- package/dist/protocol/parent.d.ts.map +0 -1
- package/dist/protocol/protocolVersion.d.ts.map +0 -1
- package/dist/protocol/theme.d.ts.map +0 -1
- 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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
79
|
+
const limit = resolveDataSourceQueryLimit(queryOptions?.limit, warn)
|
|
72
80
|
if (limit.status === "error") {
|
|
73
81
|
return limit
|
|
74
82
|
}
|
|
75
|
-
const filter = resolveFilter(
|
|
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 (
|
|
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
|
-
|
|
154
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
246
|
+
return resolvePropertyFilter(filterObject, dataSource)
|
|
177
247
|
}
|
|
178
248
|
|
|
179
|
-
function
|
|
180
|
-
|
|
249
|
+
function resolveSorts(
|
|
250
|
+
sorts: unknown,
|
|
181
251
|
dataSource: NotionDataSource,
|
|
182
252
|
):
|
|
183
|
-
| { status: "ok";
|
|
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 (
|
|
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
|
|
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(
|
|
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 =
|
|
207
|
-
if (
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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: "
|
|
341
|
-
|
|
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"
|
|
353
|
-
const hasPropertyId =
|
|
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 (
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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 (
|
|
431
|
-
|
|
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
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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 {
|