@notionhq/custom-blocks 0.1.32 → 0.1.34
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/bridge/SandboxBridge.d.ts +4 -1
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +95 -57
- package/dist/bridge/dataSources/query.d.ts +26 -0
- package/dist/bridge/dataSources/query.d.ts.map +1 -0
- package/dist/bridge/dataSources/query.js +377 -0
- package/dist/bridge/hostState.d.ts +4 -3
- package/dist/bridge/hostState.d.ts.map +1 -1
- package/dist/bridge/hostState.js +7 -3
- package/dist/bridge/sandboxClient.d.ts +4 -2
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +10 -4
- package/dist/protocol/messages/init.d.ts +1 -1
- package/dist/protocol/messages/init.d.ts.map +1 -1
- package/dist/protocol/messages/initResult.d.ts +8 -1
- package/dist/protocol/messages/initResult.d.ts.map +1 -1
- package/dist/protocol/messages/queryDataSource.d.ts +955 -0
- package/dist/protocol/messages/queryDataSource.d.ts.map +1 -1
- package/dist/protocol/messages/queryDataSource.js +100 -0
- package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -1
- package/dist/protocol/messages/queryDataSourceResult.d.ts.map +1 -1
- package/dist/protocol/messages/sandboxToHost.d.ts +349 -0
- package/dist/protocol/messages/sandboxToHost.d.ts.map +1 -1
- package/dist/react/useDataSource.d.ts.map +1 -1
- package/dist/react/useDataSource.js +10 -5
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/data-sources.md +39 -3
- package/docs/errors.md +16 -0
- package/docs/lifecycle.md +74 -20
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +112 -64
- package/src/bridge/dataSources/query.ts +470 -0
- package/src/bridge/hostState.ts +11 -3
- package/src/bridge/sandboxClient.ts +20 -4
- package/src/react/useDataSource.ts +16 -5
- package/src/types.ts +47 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import type { NotionDataSource } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
|
|
2
|
+
import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
|
|
3
|
+
import {
|
|
4
|
+
customBlockCheckboxFilterOperatorSchema,
|
|
5
|
+
customBlockContainsFilterOperatorSchema,
|
|
6
|
+
customBlockDateFilterOperatorSchema,
|
|
7
|
+
customBlockNumberFilterOperatorSchema,
|
|
8
|
+
customBlockOptionFilterOperatorSchema,
|
|
9
|
+
customBlockTextFilterOperatorSchema,
|
|
10
|
+
type ResolvedCustomBlockDataSourceFilter,
|
|
11
|
+
type ResolvedCustomBlockDataSourcePropertyFilter,
|
|
12
|
+
} from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
|
|
13
|
+
import * as v from "valibot"
|
|
14
|
+
import type { UseDataSourceOptions } from "../../types.js"
|
|
15
|
+
|
|
16
|
+
const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20
|
|
17
|
+
const MAX_DATA_SOURCE_QUERY_LIMIT = 999
|
|
18
|
+
const MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN = 25
|
|
19
|
+
|
|
20
|
+
export type ResolvedDataSourceQuery = {
|
|
21
|
+
dataSourceId: NotionDataSourceId
|
|
22
|
+
limit: number
|
|
23
|
+
filter?: ResolvedCustomBlockDataSourceFilter
|
|
24
|
+
identity: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ResolveDataSourceQueryResult =
|
|
28
|
+
| { status: "error"; error: string }
|
|
29
|
+
| {
|
|
30
|
+
status: "ok"
|
|
31
|
+
dataSource: NotionDataSource
|
|
32
|
+
query: ResolvedDataSourceQuery
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveDataSourceQuery(args: {
|
|
36
|
+
dataSources: NotionDataSource[]
|
|
37
|
+
key: string
|
|
38
|
+
options?: unknown
|
|
39
|
+
warn?: (message: string) => void
|
|
40
|
+
}): ResolveDataSourceQueryResult {
|
|
41
|
+
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
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const dataSource = dataSources.find(entry => entry.key === key)
|
|
58
|
+
if (dataSource === undefined) {
|
|
59
|
+
return {
|
|
60
|
+
status: "error",
|
|
61
|
+
error: `Unknown data source key "${key}". Known keys: [${dataSources.map(entry => entry.key).join(", ")}].`,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (dataSource.collectionPointer === undefined) {
|
|
65
|
+
return {
|
|
66
|
+
status: "error",
|
|
67
|
+
error: `Data source "${key}" has not been mapped to a database yet.`,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const limit = resolveDataSourceQueryLimit(options?.limit, warn)
|
|
72
|
+
if (limit.status === "error") {
|
|
73
|
+
return limit
|
|
74
|
+
}
|
|
75
|
+
const filter = resolveFilter(options?.filter, dataSource)
|
|
76
|
+
if (filter.status === "error") {
|
|
77
|
+
return filter
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const identity = stableJsonStringify({
|
|
81
|
+
dataSourceId: dataSource.collectionPointer.id,
|
|
82
|
+
limit: limit.limit,
|
|
83
|
+
filter: filter.filter ?? null,
|
|
84
|
+
})
|
|
85
|
+
return {
|
|
86
|
+
status: "ok",
|
|
87
|
+
dataSource,
|
|
88
|
+
query: {
|
|
89
|
+
dataSourceId: dataSource.collectionPointer.id,
|
|
90
|
+
limit: limit.limit,
|
|
91
|
+
filter: filter.filter,
|
|
92
|
+
identity,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveDataSourceQueryLimit(
|
|
98
|
+
limit: unknown,
|
|
99
|
+
warn: ((message: string) => void) | undefined = console.warn,
|
|
100
|
+
): { status: "ok"; limit: number } | { status: "error"; error: string } {
|
|
101
|
+
if (limit === undefined) {
|
|
102
|
+
return { status: "ok", limit: DEFAULT_DATA_SOURCE_QUERY_LIMIT }
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
typeof limit !== "number" ||
|
|
106
|
+
!Number.isFinite(limit) ||
|
|
107
|
+
!Number.isInteger(limit) ||
|
|
108
|
+
limit < 1
|
|
109
|
+
) {
|
|
110
|
+
return {
|
|
111
|
+
status: "error",
|
|
112
|
+
error: `Data source query limit must be a positive integer between 1 and ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
|
|
116
|
+
warn?.(
|
|
117
|
+
`Data source query limit ${limit} exceeds the maximum of ${MAX_DATA_SOURCE_QUERY_LIMIT}; clamping to ${MAX_DATA_SOURCE_QUERY_LIMIT}.`,
|
|
118
|
+
)
|
|
119
|
+
return { status: "ok", limit: MAX_DATA_SOURCE_QUERY_LIMIT }
|
|
120
|
+
}
|
|
121
|
+
return { status: "ok", limit }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getDataSourceQueryOptionsIdentity(
|
|
125
|
+
options: UseDataSourceOptions | undefined,
|
|
126
|
+
): string {
|
|
127
|
+
try {
|
|
128
|
+
return stableJsonStringify(options ?? {})
|
|
129
|
+
} catch {
|
|
130
|
+
return "invalid-data-source-query-options"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stableJsonStringify(value: unknown): string {
|
|
135
|
+
return JSON.stringify(normalizeJsonValue(value))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveFilter(
|
|
139
|
+
filter: unknown,
|
|
140
|
+
dataSource: NotionDataSource,
|
|
141
|
+
):
|
|
142
|
+
| { status: "ok"; filter?: ResolvedCustomBlockDataSourceFilter }
|
|
143
|
+
| { status: "error"; error: string } {
|
|
144
|
+
if (filter === undefined) {
|
|
145
|
+
return { status: "ok" }
|
|
146
|
+
}
|
|
147
|
+
if (!isPlainRecord(filter)) {
|
|
148
|
+
return {
|
|
149
|
+
status: "error",
|
|
150
|
+
error: "Data source query filter must be an object.",
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ("and" in filter) {
|
|
154
|
+
if (Object.keys(filter).length !== 1 || !Array.isArray(filter.and)) {
|
|
155
|
+
return {
|
|
156
|
+
status: "error",
|
|
157
|
+
error: 'Data source query filter "and" must be an array.',
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (filter.and.length > MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN) {
|
|
161
|
+
return {
|
|
162
|
+
status: "error",
|
|
163
|
+
error: `Data source query filter "and" may contain at most ${MAX_DATA_SOURCE_QUERY_FILTER_CHILDREN} children.`,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const and: ResolvedCustomBlockDataSourcePropertyFilter[] = []
|
|
167
|
+
for (const child of filter.and) {
|
|
168
|
+
const resolved = resolvePropertyFilter(child, dataSource)
|
|
169
|
+
if (resolved.status === "error") {
|
|
170
|
+
return resolved
|
|
171
|
+
}
|
|
172
|
+
and.push(resolved.filter)
|
|
173
|
+
}
|
|
174
|
+
return { status: "ok", filter: { and } }
|
|
175
|
+
}
|
|
176
|
+
return resolvePropertyFilter(filter, dataSource)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolvePropertyFilter(
|
|
180
|
+
filter: unknown,
|
|
181
|
+
dataSource: NotionDataSource,
|
|
182
|
+
):
|
|
183
|
+
| { status: "ok"; filter: ResolvedCustomBlockDataSourcePropertyFilter }
|
|
184
|
+
| { status: "error"; error: string } {
|
|
185
|
+
if (!isPlainRecord(filter)) {
|
|
186
|
+
return {
|
|
187
|
+
status: "error",
|
|
188
|
+
error: "Data source query property filter must be an object.",
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const property = resolvePropertyAddress(filter, dataSource)
|
|
192
|
+
if (property.status === "error") {
|
|
193
|
+
return property
|
|
194
|
+
}
|
|
195
|
+
const branchKeys = Object.keys(filter).filter(
|
|
196
|
+
key => key !== "key" && key !== "propertyId",
|
|
197
|
+
)
|
|
198
|
+
if (branchKeys.length !== 1) {
|
|
199
|
+
return {
|
|
200
|
+
status: "error",
|
|
201
|
+
error:
|
|
202
|
+
"Data source query property filter must contain exactly one property branch.",
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const branch = branchKeys[0]
|
|
206
|
+
const value = filter[branch]
|
|
207
|
+
if (!isPlainRecord(value) || Object.keys(value).length !== 1) {
|
|
208
|
+
return invalidOperator(branch)
|
|
209
|
+
}
|
|
210
|
+
if (property.propertyType !== branch) {
|
|
211
|
+
return {
|
|
212
|
+
status: "error",
|
|
213
|
+
error: `Data source query filter branch "${branch}" does not match property type "${property.propertyType}".`,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
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
|
+
}
|
|
239
|
+
case "url":
|
|
240
|
+
case "email":
|
|
241
|
+
case "phone_number": {
|
|
242
|
+
const parsed = v.safeParse(customBlockTextFilterOperatorSchema, value)
|
|
243
|
+
if (!parsed.success) {
|
|
244
|
+
return invalidOperator(branch)
|
|
245
|
+
}
|
|
246
|
+
if (branch === "url") {
|
|
247
|
+
return {
|
|
248
|
+
status: "ok",
|
|
249
|
+
filter: { propertyId: property.propertyId, url: parsed.output },
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (branch === "email") {
|
|
253
|
+
return {
|
|
254
|
+
status: "ok",
|
|
255
|
+
filter: { propertyId: property.propertyId, email: parsed.output },
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
status: "ok",
|
|
260
|
+
filter: {
|
|
261
|
+
propertyId: property.propertyId,
|
|
262
|
+
phone_number: parsed.output,
|
|
263
|
+
},
|
|
264
|
+
}
|
|
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
|
+
}
|
|
278
|
+
}
|
|
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
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return invalidOperator(branch)
|
|
337
|
+
}
|
|
338
|
+
default:
|
|
339
|
+
return {
|
|
340
|
+
status: "error",
|
|
341
|
+
error: `Data source query filter branch "${branch}" is not supported.`,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function resolvePropertyAddress(
|
|
347
|
+
value: Record<string, unknown>,
|
|
348
|
+
dataSource: NotionDataSource,
|
|
349
|
+
):
|
|
350
|
+
| { status: "ok"; propertyId: string; propertyType: string }
|
|
351
|
+
| { status: "error"; error: string } {
|
|
352
|
+
const hasKey = "key" in value
|
|
353
|
+
const hasPropertyId = "propertyId" in value
|
|
354
|
+
if (hasKey === hasPropertyId) {
|
|
355
|
+
return {
|
|
356
|
+
status: "error",
|
|
357
|
+
error:
|
|
358
|
+
"Data source query filters must use exactly one of key or propertyId.",
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
let propertyId: string
|
|
362
|
+
if (hasKey) {
|
|
363
|
+
if (typeof value.key !== "string") {
|
|
364
|
+
return {
|
|
365
|
+
status: "error",
|
|
366
|
+
error: "Data source query property key must be a string.",
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!(value.key in dataSource.propertyIdsByKey)) {
|
|
370
|
+
return {
|
|
371
|
+
status: "error",
|
|
372
|
+
error: `Unknown property key "${value.key}" for data source "${dataSource.key}".`,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const resolvedPropertyId = dataSource.propertyIdsByKey[value.key]
|
|
376
|
+
if (resolvedPropertyId === undefined) {
|
|
377
|
+
return {
|
|
378
|
+
status: "error",
|
|
379
|
+
error: `Property key "${value.key}" for data source "${dataSource.key}" is not bound.`,
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
propertyId = resolvedPropertyId
|
|
383
|
+
} else {
|
|
384
|
+
if (typeof value.propertyId !== "string") {
|
|
385
|
+
return {
|
|
386
|
+
status: "error",
|
|
387
|
+
error: "Data source query propertyId must be a string.",
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
propertyId = value.propertyId
|
|
391
|
+
}
|
|
392
|
+
const propertySchema = dataSource.propertySchemasById[propertyId]
|
|
393
|
+
if (propertySchema === undefined) {
|
|
394
|
+
return {
|
|
395
|
+
status: "error",
|
|
396
|
+
error: `Unknown property ID "${propertyId}" for data source "${dataSource.key}".`,
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
status: "ok",
|
|
401
|
+
propertyId,
|
|
402
|
+
propertyType: propertySchema.type,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function invalidOperator(branch: string): { status: "error"; error: string } {
|
|
407
|
+
return {
|
|
408
|
+
status: "error",
|
|
409
|
+
error: `Data source query filter branch "${branch}" has an invalid operator or value.`,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function normalizeJsonValue(value: unknown): unknown {
|
|
414
|
+
if (
|
|
415
|
+
value === null ||
|
|
416
|
+
typeof value === "string" ||
|
|
417
|
+
typeof value === "boolean"
|
|
418
|
+
) {
|
|
419
|
+
return value
|
|
420
|
+
}
|
|
421
|
+
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
|
|
426
|
+
}
|
|
427
|
+
if (Array.isArray(value)) {
|
|
428
|
+
return value.map(entry => normalizeJsonValue(entry))
|
|
429
|
+
}
|
|
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
|
|
439
|
+
}
|
|
440
|
+
throw new Error("Data source query values must be JSON-compatible.")
|
|
441
|
+
}
|
|
442
|
+
|
|
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
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isValidIsoDate(value: unknown): boolean {
|
|
454
|
+
if (typeof value !== "string") {
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
457
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
|
458
|
+
if (dateOnly !== null) {
|
|
459
|
+
const [, year, month, day] = dateOnly
|
|
460
|
+
const date = new Date(
|
|
461
|
+
Date.UTC(Number(year), Number(month) - 1, Number(day)),
|
|
462
|
+
)
|
|
463
|
+
return date.toISOString().slice(0, 10) === value
|
|
464
|
+
}
|
|
465
|
+
return (
|
|
466
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
|
467
|
+
value,
|
|
468
|
+
) && Number.isFinite(Date.parse(value))
|
|
469
|
+
)
|
|
470
|
+
}
|
package/src/bridge/hostState.ts
CHANGED
|
@@ -41,17 +41,21 @@ export type InitializedHostState = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export type DataSourceQueryState = {
|
|
44
|
+
dataSourceKey: string
|
|
44
45
|
/** Latest pages from the host as parsed from the bridge. `propertiesByKey` is derived lazily. */
|
|
45
46
|
items: NotionDataSourcePageBridge[]
|
|
46
47
|
isLoading: boolean
|
|
47
48
|
hasMore: boolean
|
|
48
49
|
error?: CustomBlockQueryDataSourceErrorInfo
|
|
49
|
-
subscriptionId?: string
|
|
50
50
|
latestLimit?: number
|
|
51
|
+
latestQueryIdentity?: string
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function createEmptyDataSourceQueryState(
|
|
54
|
+
export function createEmptyDataSourceQueryState(
|
|
55
|
+
dataSourceKey: string,
|
|
56
|
+
): DataSourceQueryState {
|
|
54
57
|
return {
|
|
58
|
+
dataSourceKey,
|
|
55
59
|
items: [],
|
|
56
60
|
isLoading: false,
|
|
57
61
|
hasMore: false,
|
|
@@ -97,6 +101,7 @@ export type UpdateDataSourcePageFn = (args: {
|
|
|
97
101
|
export function getDataSourceQueryView(
|
|
98
102
|
hostState: CustomBlockHostState,
|
|
99
103
|
key: string,
|
|
104
|
+
subscriptionId: string,
|
|
100
105
|
updateDataSourcePage: UpdateDataSourcePageFn,
|
|
101
106
|
): DataSourceQueryView {
|
|
102
107
|
if (hostState.status !== "initialized") {
|
|
@@ -104,8 +109,11 @@ export function getDataSourceQueryView(
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
const dataSource = hostState.dataSources.find(entry => entry.key === key)
|
|
112
|
+
const subscriptionState = hostState.dataSourceState[subscriptionId]
|
|
107
113
|
const queryState =
|
|
108
|
-
|
|
114
|
+
subscriptionState?.dataSourceKey === key
|
|
115
|
+
? subscriptionState
|
|
116
|
+
: createEmptyDataSourceQueryState(key)
|
|
109
117
|
|
|
110
118
|
if (dataSource === undefined) {
|
|
111
119
|
return {
|
|
@@ -68,16 +68,32 @@ export const customBlockHost = {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export const customBlockDataSources = {
|
|
71
|
-
|
|
72
|
-
getBridge().
|
|
71
|
+
createSubscriptionId: () => {
|
|
72
|
+
return getBridge().createDataSourceSubscriptionId()
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
query: (
|
|
76
|
+
subscriptionId: string,
|
|
77
|
+
key: string,
|
|
78
|
+
options?: UseDataSourceOptions,
|
|
79
|
+
) => {
|
|
80
|
+
getBridge().queryDataSource(subscriptionId, key, options)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
release: (subscriptionId: string) => {
|
|
84
|
+
getBridge().releaseDataSourceSubscription(subscriptionId)
|
|
73
85
|
},
|
|
74
86
|
|
|
75
87
|
getView: (
|
|
76
88
|
hostState: CustomBlockHostState,
|
|
77
89
|
key: string,
|
|
90
|
+
subscriptionId: string,
|
|
78
91
|
): DataSourceQueryView => {
|
|
79
|
-
return getDataSourceQueryViewWithBridge(
|
|
80
|
-
|
|
92
|
+
return getDataSourceQueryViewWithBridge(
|
|
93
|
+
hostState,
|
|
94
|
+
key,
|
|
95
|
+
subscriptionId,
|
|
96
|
+
args => getBridge().updateDataSourcePage(args),
|
|
81
97
|
)
|
|
82
98
|
},
|
|
83
99
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect } from "react"
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { getDataSourceQueryOptionsIdentity } from "../bridge/dataSources/query.js"
|
|
2
3
|
import { customBlockDataSources } from "../bridge/sandboxClient.js"
|
|
3
4
|
import type { UseDataSourceOptions, UseDataSourceResult } from "../types.js"
|
|
4
5
|
import { useCustomBlockHost } from "./useHostState.js"
|
|
@@ -18,7 +19,10 @@ export function useDataSource(
|
|
|
18
19
|
options?: UseDataSourceOptions,
|
|
19
20
|
): UseDataSourceResult {
|
|
20
21
|
const host = useCustomBlockHost()
|
|
21
|
-
const
|
|
22
|
+
const [subscriptionId] = useState(() =>
|
|
23
|
+
customBlockDataSources.createSubscriptionId(),
|
|
24
|
+
)
|
|
25
|
+
const optionsIdentity = getDataSourceQueryOptionsIdentity(options)
|
|
22
26
|
const matchingDataSource =
|
|
23
27
|
host.status === "initialized"
|
|
24
28
|
? host.dataSources.find(dataSource => dataSource.key === key)
|
|
@@ -30,10 +34,17 @@ export function useDataSource(
|
|
|
30
34
|
return
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
customBlockDataSources.query(key,
|
|
34
|
-
}, [matchingDataSource, isInitialized, key,
|
|
37
|
+
customBlockDataSources.query(subscriptionId, key, options)
|
|
38
|
+
}, [matchingDataSource, isInitialized, key, optionsIdentity, subscriptionId])
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
useEffect(
|
|
41
|
+
() => () => {
|
|
42
|
+
customBlockDataSources.release(subscriptionId)
|
|
43
|
+
},
|
|
44
|
+
[subscriptionId],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const view = customBlockDataSources.getView(host, key, subscriptionId)
|
|
37
48
|
|
|
38
49
|
return {
|
|
39
50
|
items: view.items,
|
package/src/types.ts
CHANGED
|
@@ -24,6 +24,14 @@ import type {
|
|
|
24
24
|
ListUsersMessage,
|
|
25
25
|
ListUsersResultMessage,
|
|
26
26
|
} from "@notionhq/custom-blocks-protocol/messages/listUsers.js"
|
|
27
|
+
import type {
|
|
28
|
+
CustomBlockCheckboxFilterOperator,
|
|
29
|
+
CustomBlockContainsFilterOperator,
|
|
30
|
+
CustomBlockDateFilterOperator,
|
|
31
|
+
CustomBlockNumberFilterOperator,
|
|
32
|
+
CustomBlockOptionFilterOperator,
|
|
33
|
+
CustomBlockTextFilterOperator,
|
|
34
|
+
} from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
|
|
27
35
|
import type { CustomBlockQueryDataSourceErrorInfo } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
|
|
28
36
|
import type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
|
|
29
37
|
import type {
|
|
@@ -116,6 +124,40 @@ export type NotionDataSourcePageUpdateInput = NotionDataSourcePageUpdateArgs
|
|
|
116
124
|
*/
|
|
117
125
|
export type NotionDataSourcePageUpdateResult = UpdatePageResult
|
|
118
126
|
|
|
127
|
+
export type NotionDataSourceTextFilterOperator = CustomBlockTextFilterOperator
|
|
128
|
+
export type NotionDataSourceNumberFilterOperator =
|
|
129
|
+
CustomBlockNumberFilterOperator
|
|
130
|
+
export type NotionDataSourceCheckboxFilterOperator =
|
|
131
|
+
CustomBlockCheckboxFilterOperator
|
|
132
|
+
export type NotionDataSourceOptionFilterOperator =
|
|
133
|
+
CustomBlockOptionFilterOperator
|
|
134
|
+
export type NotionDataSourceContainsFilterOperator =
|
|
135
|
+
CustomBlockContainsFilterOperator
|
|
136
|
+
export type NotionDataSourceDateFilterOperator = CustomBlockDateFilterOperator
|
|
137
|
+
|
|
138
|
+
export type NotionDataSourcePropertyAddress =
|
|
139
|
+
| { key: string; propertyId?: never }
|
|
140
|
+
| { propertyId: string; key?: never }
|
|
141
|
+
|
|
142
|
+
export type NotionDataSourcePropertyFilter = NotionDataSourcePropertyAddress &
|
|
143
|
+
(
|
|
144
|
+
| { title: NotionDataSourceTextFilterOperator }
|
|
145
|
+
| { rich_text: NotionDataSourceTextFilterOperator }
|
|
146
|
+
| { url: NotionDataSourceTextFilterOperator }
|
|
147
|
+
| { email: NotionDataSourceTextFilterOperator }
|
|
148
|
+
| { phone_number: NotionDataSourceTextFilterOperator }
|
|
149
|
+
| { number: NotionDataSourceNumberFilterOperator }
|
|
150
|
+
| { checkbox: NotionDataSourceCheckboxFilterOperator }
|
|
151
|
+
| { select: NotionDataSourceOptionFilterOperator }
|
|
152
|
+
| { multi_select: NotionDataSourceContainsFilterOperator }
|
|
153
|
+
| { status: NotionDataSourceOptionFilterOperator }
|
|
154
|
+
| { date: NotionDataSourceDateFilterOperator }
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
export type NotionDataSourceFilter =
|
|
158
|
+
| NotionDataSourcePropertyFilter
|
|
159
|
+
| { and: NotionDataSourcePropertyFilter[] }
|
|
160
|
+
|
|
119
161
|
/**
|
|
120
162
|
* Return shape of `useDataSource`.
|
|
121
163
|
*
|
|
@@ -157,6 +199,11 @@ export type UseDataSourceOptions = {
|
|
|
157
199
|
* Maximum number of rows to request from the host. Defaults to 20.
|
|
158
200
|
*/
|
|
159
201
|
limit?: number
|
|
202
|
+
/**
|
|
203
|
+
* Optional property filter. The SDK resolves semantic property keys before
|
|
204
|
+
* it sends the query to the host.
|
|
205
|
+
*/
|
|
206
|
+
filter?: NotionDataSourceFilter
|
|
160
207
|
}
|
|
161
208
|
|
|
162
209
|
/**
|