@notionhq/custom-blocks-host 0.1.24 → 0.1.26

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 (37) hide show
  1. package/README.md +13 -8
  2. package/dist/createCustomBlockHost.d.ts +8 -6
  3. package/dist/createCustomBlockHost.d.ts.map +1 -1
  4. package/dist/createCustomBlockHost.js +31 -18
  5. package/dist/lifecycle/normalizeHostToSandboxMessage.d.ts +6 -0
  6. package/dist/lifecycle/normalizeHostToSandboxMessage.d.ts.map +1 -0
  7. package/dist/lifecycle/normalizeHostToSandboxMessage.js +38 -0
  8. package/dist/protocol/dataSources/dataSource.d.ts +61 -6
  9. package/dist/protocol/dataSources/dataSource.js +35 -6
  10. package/dist/protocol/dataSources/propertySchema.d.ts +2 -0
  11. package/dist/protocol/dataSources/propertySchema.js +7 -0
  12. package/dist/protocol/messages/createPageResult.d.ts +1 -1
  13. package/dist/protocol/messages/getPage.d.ts +1 -1
  14. package/dist/protocol/messages/hostToSandbox.d.ts +10 -4
  15. package/dist/protocol/messages/init.d.ts +5 -2
  16. package/dist/protocol/messages/openPage.d.ts +1 -1
  17. package/dist/protocol/messages/pageChanged.d.ts +5 -2
  18. package/dist/protocol/messages/queryDataSource.d.ts +586 -485
  19. package/dist/protocol/messages/queryDataSource.js +61 -43
  20. package/dist/protocol/messages/queryDataSourceResult.d.ts +1 -1
  21. package/dist/protocol/messages/sandboxToHost.d.ts +212 -178
  22. package/dist/protocol/messages/updatePageResult.d.ts +1 -1
  23. package/dist/protocol/pages/page.d.ts +5 -2
  24. package/dist/protocol/pages/page.js +15 -9
  25. package/dist/protocol/protocolVersion.d.ts +1 -1
  26. package/dist/protocol/protocolVersion.js +1 -1
  27. package/dist/queries/validateQueryDataSourceFilter.d.ts +1 -1
  28. package/dist/queries/validateQueryDataSourceFilter.d.ts.map +1 -1
  29. package/dist/queries/validateQueryDataSourceFilter.js +11 -6
  30. package/dist/queries/validateQueryDataSourceSorts.d.ts +1 -1
  31. package/dist/queries/validateQueryDataSourceSorts.d.ts.map +1 -1
  32. package/dist/queries/validateQueryDataSourceSorts.js +3 -5
  33. package/package.json +1 -1
  34. package/src/createCustomBlockHost.ts +54 -31
  35. package/src/lifecycle/normalizeHostToSandboxMessage.ts +46 -0
  36. package/src/queries/validateQueryDataSourceFilter.ts +12 -7
  37. package/src/queries/validateQueryDataSourceSorts.ts +4 -10
@@ -8,7 +8,10 @@ import * as v from "valibot";
8
8
  /**
9
9
  * The custom block's nearest enclosing page ancestor, carried in `init.page` and `pageChanged`.
10
10
  */
11
- export declare const customBlockPageSchema: v.ObjectSchema<{
11
+ export declare const customBlockPageSchema: v.VariantSchema<"type", [v.ObjectSchema<{
12
+ readonly type: v.LiteralSchema<"unavailable", undefined>;
13
+ }, undefined>, v.ObjectSchema<{
14
+ readonly type: v.LiteralSchema<"page", undefined>;
12
15
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.BrandAction<string, "NotionPageId">]>;
13
16
  /**
14
17
  * The containing page's own parent. It lets a block distinguish a freestanding page from a
@@ -33,7 +36,7 @@ export declare const customBlockPageSchema: v.ObjectSchema<{
33
36
  }, undefined>, v.ObjectSchema<{
34
37
  readonly type: v.LiteralSchema<"unavailable", undefined>;
35
38
  }, undefined>], undefined>;
36
- }, undefined>;
39
+ }, undefined>], undefined>;
37
40
  export type CustomBlockPage = v.InferOutput<typeof customBlockPageSchema>;
38
41
  export declare const notionEmojiIconSchema: v.ObjectSchema<{
39
42
  readonly type: v.LiteralSchema<"emoji", undefined>;
@@ -10,15 +10,21 @@ import { notionParentSchema } from "../parent.js";
10
10
  /**
11
11
  * The custom block's nearest enclosing page ancestor, carried in `init.page` and `pageChanged`.
12
12
  */
13
- export const customBlockPageSchema = v.object({
14
- id: notionPageIdSchema,
15
- /**
16
- * The containing page's own parent. It lets a block distinguish a freestanding page from a
17
- * database row without a round trip. Not to be confused with the top-level `parent` runtime
18
- * value, which is the custom block's own parent.
19
- */
20
- parent: notionParentSchema,
21
- });
13
+ export const customBlockPageSchema = v.variant("type", [
14
+ /** The viewer cannot access the containing page; its ID and parent are not exposed. */
15
+ v.object({ type: v.literal("unavailable") }),
16
+ /** The viewer can access the containing page; includes its ID and parent location. */
17
+ v.object({
18
+ type: v.literal("page"),
19
+ id: notionPageIdSchema,
20
+ /**
21
+ * The containing page's own parent. It lets a block distinguish a freestanding page from a
22
+ * database row without a round trip. Not to be confused with the top-level `parent` runtime
23
+ * value, which is the custom block's own parent.
24
+ */
25
+ parent: notionParentSchema,
26
+ }),
27
+ ]);
22
28
  export const notionEmojiIconSchema = v.object({
23
29
  type: v.literal("emoji"),
24
30
  emoji: v.string(),
@@ -3,4 +3,4 @@
3
3
  * Increase this value for breaking wire changes. Hosts can continue to accept
4
4
  * older versions, so this value does not set the minimum or maximum they accept.
5
5
  */
6
- export declare const LATEST_BRIDGE_PROTOCOL_VERSION = 4;
6
+ export declare const LATEST_BRIDGE_PROTOCOL_VERSION = 5;
@@ -3,4 +3,4 @@
3
3
  * Increase this value for breaking wire changes. Hosts can continue to accept
4
4
  * older versions, so this value does not set the minimum or maximum they accept.
5
5
  */
6
- export const LATEST_BRIDGE_PROTOCOL_VERSION = 4;
6
+ export const LATEST_BRIDGE_PROTOCOL_VERSION = 5;
@@ -1,5 +1,5 @@
1
1
  import type { NotionCollectionSchema } from "../protocol/dataSources/dataSource.js";
2
2
  import type { ResolvedCustomBlockDataSourceFilter } from "../protocol/messages/queryDataSource.js";
3
3
  import type { CustomBlockQueryDataSourceErrorInfo } from "../protocol/messages/queryDataSourceResult.js";
4
- export declare function validateQueryDataSourceFilter(filter: ResolvedCustomBlockDataSourceFilter | undefined, collectionSchema: NotionCollectionSchema | undefined): CustomBlockQueryDataSourceErrorInfo | undefined;
4
+ export declare function validateQueryDataSourceFilter(filter: ResolvedCustomBlockDataSourceFilter | undefined, collectionSchema: NotionCollectionSchema): CustomBlockQueryDataSourceErrorInfo | undefined;
5
5
  //# sourceMappingURL=validateQueryDataSourceFilter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateQueryDataSourceFilter.d.ts","sourceRoot":"","sources":["../../src/queries/validateQueryDataSourceFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4DAA4D,CAAA;AACxG,OAAO,KAAK,EAGX,mCAAmC,EAEnC,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,oEAAoE,CAAA;AAO7H,wBAAgB,6BAA6B,CAC5C,MAAM,EAAE,mCAAmC,GAAG,SAAS,EACvD,gBAAgB,EAAE,sBAAsB,GAAG,SAAS,GAClD,mCAAmC,GAAG,SAAS,CAajD"}
1
+ {"version":3,"file":"validateQueryDataSourceFilter.d.ts","sourceRoot":"","sources":["../../src/queries/validateQueryDataSourceFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4DAA4D,CAAA;AACxG,OAAO,KAAK,EAGX,mCAAmC,EAEnC,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,oEAAoE,CAAA;AAO7H,wBAAgB,6BAA6B,CAC5C,MAAM,EAAE,mCAAmC,GAAG,SAAS,EACvD,gBAAgB,EAAE,sBAAsB,GACtC,mCAAmC,GAAG,SAAS,CAajD"}
@@ -1,6 +1,6 @@
1
1
  import { unreachable } from "../utils.js";
2
2
  export function validateQueryDataSourceFilter(filter, collectionSchema) {
3
- if (filter === undefined || collectionSchema === undefined) {
3
+ if (filter === undefined) {
4
4
  return undefined;
5
5
  }
6
6
  const propertyFilters = "and" in filter ? filter.and : [filter];
@@ -13,14 +13,19 @@ export function validateQueryDataSourceFilter(filter, collectionSchema) {
13
13
  return undefined;
14
14
  }
15
15
  function validatePropertyFilter(filter, collectionSchema) {
16
- const propertySchema = collectionSchema.propertiesById[filter.propertyId];
17
- if (propertySchema === undefined) {
18
- return undefined;
16
+ if (!Object.hasOwn(collectionSchema.propertiesById, filter.propertyId)) {
17
+ return {
18
+ code: "invalid_data_source_query_filter",
19
+ message: `Data source query filter property ID "${filter.propertyId}" is not defined in the bound data source schema.`,
20
+ isRetryable: false,
21
+ };
19
22
  }
20
- if ("people" in filter && propertySchema.type !== "people") {
23
+ const propertySchema = collectionSchema.propertiesById[filter.propertyId];
24
+ const branch = Object.keys(filter).find(key => key !== "propertyId");
25
+ if (branch !== propertySchema.type) {
21
26
  return {
22
27
  code: "invalid_data_source_query_filter",
23
- message: `Data source query filter branch "people" does not match property type "${propertySchema.type}".`,
28
+ message: `Data source query filter branch "${branch}" does not match property type "${propertySchema.type}".`,
24
29
  isRetryable: false,
25
30
  };
26
31
  }
@@ -4,5 +4,5 @@ import type { CustomBlockQueryDataSourceErrorInfo } from "../protocol/messages/q
4
4
  /**
5
5
  * Ensures the sorts of a data source query are valid.
6
6
  */
7
- export declare function validateQueryDataSourceSorts(sorts: ResolvedCustomBlockDataSourceSort[] | undefined, collectionSchema: NotionCollectionSchema | undefined): CustomBlockQueryDataSourceErrorInfo | undefined;
7
+ export declare function validateQueryDataSourceSorts(sorts: ResolvedCustomBlockDataSourceSort[] | undefined, collectionSchema: NotionCollectionSchema): CustomBlockQueryDataSourceErrorInfo | undefined;
8
8
  //# sourceMappingURL=validateQueryDataSourceSorts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validateQueryDataSourceSorts.d.ts","sourceRoot":"","sources":["../../src/queries/validateQueryDataSourceSorts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4DAA4D,CAAA;AAExG,OAAO,EAEN,KAAK,iCAAiC,EACtC,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,oEAAoE,CAAA;AAM7H;;GAEG;AACH,wBAAgB,4BAA4B,CAC3C,KAAK,EAAE,iCAAiC,EAAE,GAAG,SAAS,EACtD,gBAAgB,EAAE,sBAAsB,GAAG,SAAS,GAClD,mCAAmC,GAAG,SAAS,CA+CjD"}
1
+ {"version":3,"file":"validateQueryDataSourceSorts.d.ts","sourceRoot":"","sources":["../../src/queries/validateQueryDataSourceSorts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4DAA4D,CAAA;AAExG,OAAO,EAEN,KAAK,iCAAiC,EACtC,MAAM,8DAA8D,CAAA;AACrE,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,oEAAoE,CAAA;AAM7H;;GAEG;AACH,wBAAgB,4BAA4B,CAC3C,KAAK,EAAE,iCAAiC,EAAE,GAAG,SAAS,EACtD,gBAAgB,EAAE,sBAAsB,GACtC,mCAAmC,GAAG,SAAS,CAyCjD"}
@@ -25,17 +25,15 @@ export function validateQueryDataSourceSorts(sorts, collectionSchema) {
25
25
  };
26
26
  }
27
27
  seenPropertyIds.add(sort.propertyId);
28
- if (collectionSchema !== undefined &&
29
- !Object.hasOwn(collectionSchema.propertiesById, sort.propertyId)) {
28
+ if (!Object.hasOwn(collectionSchema.propertiesById, sort.propertyId)) {
30
29
  return {
31
30
  code: "invalid_data_source_query_sort",
32
31
  message: `Data source query sort property ID "${sort.propertyId}" is not defined in the bound data source schema.`,
33
32
  isRetryable: false,
34
33
  };
35
34
  }
36
- const propertySchema = collectionSchema?.propertiesById[sort.propertyId];
37
- if (propertySchema !== undefined &&
38
- !supportedDataSourceSortPropertyTypes.has(propertySchema.type)) {
35
+ const propertySchema = collectionSchema.propertiesById[sort.propertyId];
36
+ if (!supportedDataSourceSortPropertyTypes.has(propertySchema.type)) {
39
37
  return {
40
38
  code: "invalid_data_source_query_sort",
41
39
  message: `Data source query sorts do not yet support property type "${propertySchema.type}".`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks-host",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Reusable host runtime for Notion custom blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import {
2
2
  DEFAULT_CONTRAST_MODE,
3
3
  type NotionContrastMode,
4
4
  } from "@notionhq/custom-blocks-protocol/contrast.js"
5
+ import type { NotionCollectionSchema } from "@notionhq/custom-blocks-protocol/dataSources/dataSource.js"
5
6
  import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
6
7
  import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
7
8
  import type { ContrastModeChangedMessage } from "@notionhq/custom-blocks-protocol/messages/contrastModeChanged.js"
@@ -29,16 +30,21 @@ import type {
29
30
  import type { PageChangedMessage } from "@notionhq/custom-blocks-protocol/messages/pageChanged.js"
30
31
  import type { ParentChangedMessage } from "@notionhq/custom-blocks-protocol/messages/parentChanged.js"
31
32
  import type { QueryDataSourceMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
32
- import type { QueryDataSourceResultMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
33
+ import type {
34
+ CustomBlockQueryDataSourceErrorInfo,
35
+ QueryDataSourceResultMessage,
36
+ } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
33
37
  import type { ThemeChangedMessage } from "@notionhq/custom-blocks-protocol/messages/themeChanged.js"
34
38
  import type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
35
39
  import type { UpdatePageResultMessage } from "@notionhq/custom-blocks-protocol/messages/updatePageResult.js"
36
40
  import type { CustomBlockPage } from "@notionhq/custom-blocks-protocol/pages/page.js"
37
41
  import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
42
+ import { LATEST_BRIDGE_PROTOCOL_VERSION } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
38
43
  import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
39
44
  import type { NotionUser } from "@notionhq/custom-blocks-protocol/users/user.js"
40
45
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
41
46
  import { createInitializationController } from "./lifecycle/initializationController.js"
47
+ import { normalizeHostToSandboxMessage } from "./lifecycle/normalizeHostToSandboxMessage.js"
42
48
  import type {
43
49
  CustomBlockHostDataSourcesPayload,
44
50
  CustomBlockHostInitialState,
@@ -62,16 +68,19 @@ export type CustomBlockHostOptions = {
62
68
  iframe: HTMLIFrameElement
63
69
  sandboxOrigin: string
64
70
  /**
65
- * Minimum sandbox protocol version to accept. Defaults to the host runtime
66
- * minimum. Higher values retire older SDKs; lower values cannot enable
67
- * protocol versions the runtime no longer supports.
71
+ * Minimum sandbox protocol version to accept. Defaults to the host runtime minimum. Higher
72
+ * values retire older SDKs; lower values cannot enable protocol versions the runtime no longer
73
+ * supports.
68
74
  */
69
75
  minBridgeProtocolVersion?: number
70
76
  noConnectTimeoutMs?: number
71
77
  noInitResultTimeoutMs?: number
72
78
  initialState: CustomBlockHostInitialState
73
79
  handlers: CustomBlockHostHandlers
74
- /** Called as soon as the `manifest` is available after `connect`, before `onInitialized` and binding validation. */
80
+ /**
81
+ * Called as soon as the `manifest` is available after `connect`, before `onInitialized` and
82
+ * binding validation.
83
+ */
75
84
  onManifest?: (manifest: CustomBlockManifest) => void
76
85
  /** Callback fired when the initialization handshake with the sandbox completed successfully. */
77
86
  onInitialized?: (state: {
@@ -83,8 +92,7 @@ export type CustomBlockHostOptions = {
83
92
  error: CustomBlockHostInitializationFailure,
84
93
  ) => void
85
94
  /**
86
- * Callback fired when the current host and iframe must restart.
87
- * Recreate both to continue.
95
+ * Callback fired when the current host and iframe must restart. Recreate both to continue.
88
96
  */
89
97
  onRestartRequired?: () => void
90
98
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
@@ -119,8 +127,8 @@ export function createCustomBlockHost(
119
127
  noConnectTimeoutMs,
120
128
  noInitResultTimeoutMs,
121
129
  } = options
122
- // Callers can retire older SDKs, but cannot restore support for protocols
123
- // the runtime no longer implements. Omission uses the runtime minimum.
130
+ // Callers can retire older SDKs, but cannot restore support for protocols the runtime no longer
131
+ // implements. Omission uses the runtime minimum.
124
132
  const minBridgeProtocolVersion = Math.max(
125
133
  configuredMinBridgeProtocolVersion ??
126
134
  MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION,
@@ -133,6 +141,7 @@ export function createCustomBlockHost(
133
141
  let page = initialState.page
134
142
  let dataSources = initialState.dataSources
135
143
  let activeDataSources: CustomBlockHostDataSourcesPayload | undefined
144
+ let sandboxBridgeProtocolVersion = LATEST_BRIDGE_PROTOCOL_VERSION
136
145
  let manifest = initialState.manifest
137
146
  let currentUser = initialState.currentUser
138
147
  let pendingThemeChange = false
@@ -148,8 +157,12 @@ export function createCustomBlockHost(
148
157
  }
149
158
 
150
159
  function post(message: HostToSandboxMessage) {
151
- emit("out", message)
152
- iframe.contentWindow?.postMessage(message, sandboxOrigin)
160
+ const sandboxMessage = normalizeHostToSandboxMessage(
161
+ message,
162
+ sandboxBridgeProtocolVersion,
163
+ )
164
+ emit("out", sandboxMessage)
165
+ iframe.contentWindow?.postMessage(sandboxMessage, sandboxOrigin)
153
166
  }
154
167
 
155
168
  function postQueryDataSourceResult(args: {
@@ -182,28 +195,12 @@ export function createCustomBlockHost(
182
195
  activeDataSources,
183
196
  message.dataSourceId,
184
197
  )
185
- const filterError = validateQueryDataSourceFilter(
186
- message.filter,
187
- collectionSchema,
188
- )
189
- if (filterError !== undefined) {
198
+ const queryError = getQueryValidationError(message, collectionSchema)
199
+ if (queryError !== undefined) {
190
200
  if (querySubscriptions.isCurrent(message.subscriptionId, token)) {
191
201
  postQueryDataSourceResult({
192
202
  subscriptionId: message.subscriptionId,
193
- response: { status: "error", error: filterError },
194
- })
195
- }
196
- return
197
- }
198
- const sortError = validateQueryDataSourceSorts(
199
- message.sorts,
200
- collectionSchema,
201
- )
202
- if (sortError !== undefined) {
203
- if (querySubscriptions.isCurrent(message.subscriptionId, token)) {
204
- postQueryDataSourceResult({
205
- subscriptionId: message.subscriptionId,
206
- response: { status: "error", error: sortError },
203
+ response: { status: "error", error: queryError },
207
204
  })
208
205
  }
209
206
  return
@@ -321,7 +318,10 @@ export function createCustomBlockHost(
321
318
  if (pendingDataSourcesChange !== undefined) {
322
319
  const nextDataSources = pendingDataSourcesChange
323
320
  pendingDataSourcesChange = undefined
324
- post({ type: "dataSourcesChanged", dataSources: nextDataSources })
321
+ post({
322
+ type: "dataSourcesChanged",
323
+ dataSources: nextDataSources,
324
+ })
325
325
  }
326
326
  if (pendingCurrentUserChange) {
327
327
  pendingCurrentUserChange = false
@@ -339,6 +339,7 @@ export function createCustomBlockHost(
339
339
  currentUser,
340
340
  }),
341
341
  prepareInit(connect) {
342
+ sandboxBridgeProtocolVersion = connect.bridgeProtocolVersion
342
343
  manifest = connect.manifest ?? initialState.manifest
343
344
  if (manifest === undefined) {
344
345
  return {
@@ -553,3 +554,25 @@ function getBindingErrorCode(
553
554
  }
554
555
  return undefined
555
556
  }
557
+
558
+ function getQueryValidationError(
559
+ message: QueryDataSourceMessage,
560
+ collectionSchema: NotionCollectionSchema | undefined,
561
+ ): CustomBlockQueryDataSourceErrorInfo | undefined {
562
+ // The host can omit the database schema. Queries without filters or sorts need no schema validation.
563
+ if (collectionSchema === undefined) {
564
+ if (message.filter === undefined && (message.sorts?.length ?? 0) === 0) {
565
+ return undefined
566
+ }
567
+ return {
568
+ code: "data_source_schema_unavailable",
569
+ message:
570
+ "You cannot query this data source with filters or sorts because its schema is unavailable.",
571
+ isRetryable: false,
572
+ }
573
+ }
574
+ return (
575
+ validateQueryDataSourceFilter(message.filter, collectionSchema) ??
576
+ validateQueryDataSourceSorts(message.sorts, collectionSchema)
577
+ )
578
+ }
@@ -0,0 +1,46 @@
1
+ import { NOTION_BUILTIN_PROPERTY_SCHEMAS } from "@notionhq/custom-blocks-protocol/dataSources/propertySchema.js"
2
+ import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
3
+
4
+ /**
5
+ * Normalizes an outgoing message for the bridge protocol version requested by the sandbox.
6
+ */
7
+ // TODO(custom-blocks): Remove this function when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION
8
+ // reaches 5.
9
+ export function normalizeHostToSandboxMessage(
10
+ message: HostToSandboxMessage,
11
+ bridgeProtocolVersion: number,
12
+ ): HostToSandboxMessage {
13
+ // The latest SDK version does not need any normalization.
14
+ if (bridgeProtocolVersion >= 5) {
15
+ return message
16
+ }
17
+ const containsDataSources =
18
+ (message.type === "init" && message.status === "success") ||
19
+ message.type === "dataSourcesChanged"
20
+ if (!containsDataSources) {
21
+ return message
22
+ }
23
+ // Protocol 5 excludes synthetic IDs such as created_time from collectionSchema.propertiesById.
24
+ // Older SDKs derive read schemas from that map. Restore those entries in outgoing copies so
25
+ // host query validation still uses the actual database schema.
26
+ return {
27
+ ...message,
28
+ dataSources: {
29
+ bindings: Object.fromEntries(
30
+ Object.entries(message.dataSources.bindings).map(([key, binding]) => [
31
+ key,
32
+ {
33
+ ...binding,
34
+ collectionSchema: {
35
+ ...binding.collectionSchema,
36
+ propertiesById: {
37
+ ...binding.collectionSchema?.propertiesById,
38
+ ...NOTION_BUILTIN_PROPERTY_SCHEMAS,
39
+ },
40
+ },
41
+ },
42
+ ]),
43
+ ),
44
+ },
45
+ }
46
+ }
@@ -14,9 +14,9 @@ type OptionFilterOperator =
14
14
 
15
15
  export function validateQueryDataSourceFilter(
16
16
  filter: ResolvedCustomBlockDataSourceFilter | undefined,
17
- collectionSchema: NotionCollectionSchema | undefined,
17
+ collectionSchema: NotionCollectionSchema,
18
18
  ): CustomBlockQueryDataSourceErrorInfo | undefined {
19
- if (filter === undefined || collectionSchema === undefined) {
19
+ if (filter === undefined) {
20
20
  return undefined
21
21
  }
22
22
 
@@ -34,15 +34,20 @@ function validatePropertyFilter(
34
34
  filter: ResolvedCustomBlockDataSourcePropertyFilter,
35
35
  collectionSchema: NotionCollectionSchema,
36
36
  ): CustomBlockQueryDataSourceErrorInfo | undefined {
37
- const propertySchema = collectionSchema.propertiesById[filter.propertyId]
38
- if (propertySchema === undefined) {
39
- return undefined
37
+ if (!Object.hasOwn(collectionSchema.propertiesById, filter.propertyId)) {
38
+ return {
39
+ code: "invalid_data_source_query_filter",
40
+ message: `Data source query filter property ID "${filter.propertyId}" is not defined in the bound data source schema.`,
41
+ isRetryable: false,
42
+ }
40
43
  }
44
+ const propertySchema = collectionSchema.propertiesById[filter.propertyId]
41
45
 
42
- if ("people" in filter && propertySchema.type !== "people") {
46
+ const branch = Object.keys(filter).find(key => key !== "propertyId")
47
+ if (branch !== propertySchema.type) {
43
48
  return {
44
49
  code: "invalid_data_source_query_filter",
45
- message: `Data source query filter branch "people" does not match property type "${propertySchema.type}".`,
50
+ message: `Data source query filter branch "${branch}" does not match property type "${propertySchema.type}".`,
46
51
  isRetryable: false,
47
52
  }
48
53
  }
@@ -15,7 +15,7 @@ const supportedDataSourceSortPropertyTypes = new Set<string>(
15
15
  */
16
16
  export function validateQueryDataSourceSorts(
17
17
  sorts: ResolvedCustomBlockDataSourceSort[] | undefined,
18
- collectionSchema: NotionCollectionSchema | undefined,
18
+ collectionSchema: NotionCollectionSchema,
19
19
  ): CustomBlockQueryDataSourceErrorInfo | undefined {
20
20
  if (sorts === undefined) {
21
21
  return undefined
@@ -39,10 +39,7 @@ export function validateQueryDataSourceSorts(
39
39
  }
40
40
  seenPropertyIds.add(sort.propertyId)
41
41
 
42
- if (
43
- collectionSchema !== undefined &&
44
- !Object.hasOwn(collectionSchema.propertiesById, sort.propertyId)
45
- ) {
42
+ if (!Object.hasOwn(collectionSchema.propertiesById, sort.propertyId)) {
46
43
  return {
47
44
  code: "invalid_data_source_query_sort",
48
45
  message: `Data source query sort property ID "${sort.propertyId}" is not defined in the bound data source schema.`,
@@ -50,11 +47,8 @@ export function validateQueryDataSourceSorts(
50
47
  }
51
48
  }
52
49
 
53
- const propertySchema = collectionSchema?.propertiesById[sort.propertyId]
54
- if (
55
- propertySchema !== undefined &&
56
- !supportedDataSourceSortPropertyTypes.has(propertySchema.type)
57
- ) {
50
+ const propertySchema = collectionSchema.propertiesById[sort.propertyId]
51
+ if (!supportedDataSourceSortPropertyTypes.has(propertySchema.type)) {
58
52
  return {
59
53
  code: "invalid_data_source_query_sort",
60
54
  message: `Data source query sorts do not yet support property type "${propertySchema.type}".`,