ncblock 0.0.3 → 0.0.5

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 (57) hide show
  1. package/HOST.md +68 -0
  2. package/README.md +3 -0
  3. package/bridge/SandboxBridge.ts +659 -0
  4. package/bridge/context.ts +58 -0
  5. package/bridge/dataSources/dataSource.ts +63 -0
  6. package/bridge/dataSources/dataSourcePage.ts +69 -0
  7. package/bridge/dataSources/dataSourceValue.ts +19 -0
  8. package/bridge/dataSources/dateValue.ts +96 -0
  9. package/bridge/dataSources/propertySchema.ts +186 -0
  10. package/bridge/dataSources/recordPointer.ts +13 -0
  11. package/bridge/dataSources/resolve.ts +96 -0
  12. package/bridge/dataSources/resolveProperty.ts +96 -0
  13. package/bridge/hostState.ts +146 -0
  14. package/bridge/ids.ts +30 -0
  15. package/bridge/incomingType.ts +19 -0
  16. package/bridge/loadManifest.ts +54 -0
  17. package/bridge/manifest.ts +53 -0
  18. package/bridge/messages/contextChanged.ts +15 -0
  19. package/bridge/messages/createPage.ts +64 -0
  20. package/bridge/messages/createPageResult.ts +25 -0
  21. package/bridge/messages/dataSourcesChanged.ts +18 -0
  22. package/bridge/messages/getPage.ts +32 -0
  23. package/bridge/messages/getUser.ts +32 -0
  24. package/bridge/messages/hostToSandbox.ts +33 -0
  25. package/bridge/messages/init.ts +20 -0
  26. package/bridge/messages/invalidHostMessage.ts +16 -0
  27. package/bridge/messages/invalidSandboxMessage.ts +18 -0
  28. package/bridge/messages/listUsers.ts +33 -0
  29. package/bridge/messages/queryDataSource.ts +16 -0
  30. package/bridge/messages/queryDataSourceResult.ts +18 -0
  31. package/bridge/messages/ready.ts +25 -0
  32. package/bridge/messages/resize.ts +13 -0
  33. package/bridge/messages/sandboxToHost.ts +30 -0
  34. package/bridge/messages/themeChanged.ts +15 -0
  35. package/bridge/messages/updatePage.ts +21 -0
  36. package/bridge/messages/updatePageResult.ts +24 -0
  37. package/bridge/pages/page.ts +314 -0
  38. package/bridge/pendingRequests.ts +28 -0
  39. package/bridge/sandboxClient.ts +112 -0
  40. package/bridge/theme.ts +5 -0
  41. package/bridge/users/user.ts +31 -0
  42. package/docs/context.md +45 -0
  43. package/docs/data-sources.md +161 -0
  44. package/docs/lifecycle.md +92 -0
  45. package/docs/manifest.md +42 -0
  46. package/docs/pages.md +143 -0
  47. package/docs/users.md +61 -0
  48. package/host.ts +67 -0
  49. package/index.ts +86 -0
  50. package/init.ts +92 -0
  51. package/package.json +15 -5
  52. package/react.tsx +418 -0
  53. package/types.ts +157 -0
  54. package/users.ts +26 -0
  55. package/utils.ts +13 -0
  56. package/vite-plugin/index.d.ts +46 -0
  57. package/vite-plugin/index.js +115 -0
@@ -0,0 +1,63 @@
1
+ import * as v from "valibot"
2
+ import { notionDataSourceIdSchema, notionSpaceIdSchema } from "../ids"
3
+ import { notionPropertySchemaSchema } from "./propertySchema"
4
+
5
+ const collectionPointerValidator = v.object({
6
+ id: notionDataSourceIdSchema,
7
+ table: v.string(),
8
+ spaceId: v.optional(notionSpaceIdSchema),
9
+ })
10
+
11
+ export type NotionCollectionPointer = v.InferOutput<
12
+ typeof collectionPointerValidator
13
+ >
14
+
15
+ export const notionCollectionSchemaValidator = v.object({
16
+ id: v.optional(notionDataSourceIdSchema),
17
+ propertiesById: v.record(v.string(), notionPropertySchemaSchema),
18
+ })
19
+
20
+ export type NotionCollectionSchema = v.InferOutput<
21
+ typeof notionCollectionSchemaValidator
22
+ >
23
+
24
+ /**
25
+ * Bridge shape for a single data source: a semantic key bound (optionally) to a collection,
26
+ * its property name → ID mapping, and its column schemas.
27
+ */
28
+ export const notionDataSourceSchema = v.object({
29
+ key: v.string(),
30
+ collectionPointer: v.optional(collectionPointerValidator),
31
+ collectionSchema: v.optional(notionCollectionSchemaValidator),
32
+ /**
33
+ * Keyed mapping of user-defined keys to a raw property ID. A key mapped to `undefined`
34
+ * is a declared-but-unbound slot.
35
+ */
36
+ propertyIdsByKey: v.record(v.string(), v.optional(v.string())),
37
+ /**
38
+ * Keyed mapping of raw property IDs to their schema. Always includes the four built-ins
39
+ * (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`).
40
+ */
41
+ propertySchemasById: v.record(v.string(), notionPropertySchemaSchema),
42
+ })
43
+
44
+ export type NotionDataSource = v.InferOutput<typeof notionDataSourceSchema>
45
+
46
+ export const notionDataSourceBindingSchema = v.object({
47
+ collectionPointer: v.optional(collectionPointerValidator),
48
+ collectionSchema: v.optional(notionCollectionSchemaValidator),
49
+ propertyIdsByKey: v.optional(v.record(v.string(), v.optional(v.string()))),
50
+ })
51
+
52
+ export type NotionDataSourceBinding = v.InferOutput<
53
+ typeof notionDataSourceBindingSchema
54
+ >
55
+
56
+ export const notionDataSourceBindingsSchema = v.record(
57
+ v.string(),
58
+ notionDataSourceBindingSchema,
59
+ )
60
+
61
+ export type NotionDataSourceBindings = v.InferOutput<
62
+ typeof notionDataSourceBindingsSchema
63
+ >
@@ -0,0 +1,69 @@
1
+ import * as v from "valibot"
2
+ import type {
3
+ NotionPage,
4
+ NotionPageCover,
5
+ NotionPageIcon,
6
+ NotionPageId,
7
+ NotionPagePropertyInputMap,
8
+ } from "../pages/page"
9
+ import {
10
+ type NotionDataSourceValue,
11
+ notionDataSourceValueSchema,
12
+ } from "./dataSourceValue"
13
+
14
+ /**
15
+ * Bridge shape for a data source page, as parsed from inbound host messages. The host keys
16
+ * properties by raw property ID. The four built-ins (`created_time`, `last_edited_time`,
17
+ * `created_by`, `last_edited_by`) are normally present, possibly with `undefined` values.
18
+ *
19
+ * Internal to the SDK. The consumer-facing shape is {@link NotionDataSourcePage}.
20
+ */
21
+ export const notionDataSourcePageBridgeSchema = v.object({
22
+ id: v.string(),
23
+ propertiesById: v.record(v.string(), v.optional(notionDataSourceValueSchema)),
24
+ })
25
+
26
+ /** Internal SDK type for a parsed bridge page. */
27
+ export type NotionDataSourcePageBridge = v.InferOutput<
28
+ typeof notionDataSourcePageBridgeSchema
29
+ >
30
+
31
+ export type NotionDataSourcePageUpdateInput = {
32
+ properties?: NotionPagePropertyInputMap
33
+ icon?: NotionPageIcon
34
+ cover?: NotionPageCover
35
+ archived?: boolean
36
+ }
37
+
38
+ export type NotionDataSourcePageUpdateResult =
39
+ | {
40
+ status: "success"
41
+ page: NotionPage
42
+ }
43
+ | { status: "error"; error: string }
44
+
45
+ /**
46
+ * Consumer-facing page shape returned from {@link useDataSource}. Derived from the bridge payload
47
+ * plus the data source's `propertyIdsByKey`.
48
+ */
49
+ export type NotionDataSourcePage = {
50
+ id: NotionPageId
51
+ /**
52
+ * Keyed mapping of raw property IDs to their value. Includes all properties on the data source,
53
+ * including the four built-ins (`created_time`, `last_edited_time`, `created_by`,
54
+ * `last_edited_by`).
55
+ */
56
+ propertiesById: { [propertyId: string]: NotionDataSourceValue | undefined }
57
+ /**
58
+ * Keyed mapping of user-defined keys to their value. Only includes properties that are mapped
59
+ * to a raw property ID in the data source's `propertyIdsByKey`.
60
+ */
61
+ propertiesByKey: { [key: string]: NotionDataSourceValue | undefined }
62
+ /**
63
+ * Updates this page using the data source's property-key bindings. The SDK resolves property
64
+ * keys to raw property IDs before sending the bridge message to the host.
65
+ */
66
+ update: (
67
+ input: NotionDataSourcePageUpdateInput,
68
+ ) => Promise<NotionDataSourcePageUpdateResult>
69
+ }
@@ -0,0 +1,19 @@
1
+ import * as v from "valibot"
2
+ import { notionDateValueSchema } from "./dateValue"
3
+ import { notionRecordPointerSchema } from "./recordPointer"
4
+
5
+ /**
6
+ * Possible types for a data source value.
7
+ */
8
+ export const notionDataSourceValueSchema = v.union([
9
+ v.string(),
10
+ v.number(),
11
+ v.boolean(),
12
+ notionDateValueSchema,
13
+ v.array(v.string()),
14
+ v.array(notionRecordPointerSchema),
15
+ ])
16
+
17
+ export type NotionDataSourceValue = v.InferOutput<
18
+ typeof notionDataSourceValueSchema
19
+ >
@@ -0,0 +1,96 @@
1
+ import * as v from "valibot"
2
+
3
+ export const notionDateReminderSchema = v.object({
4
+ unit: v.picklist(["year", "month", "week", "day"]),
5
+ value: v.number(),
6
+ time: v.string(),
7
+ defaultTimeZone: v.optional(v.string()),
8
+ })
9
+
10
+ export type NotionDateReminder = v.InferOutput<typeof notionDateReminderSchema>
11
+
12
+ export const notionTimeReminderSchema = v.object({
13
+ unit: v.picklist(["hour", "minute"]),
14
+ value: v.number(),
15
+ })
16
+
17
+ export type NotionTimeReminder = v.InferOutput<typeof notionTimeReminderSchema>
18
+
19
+ export const notionNoReminderSchema = v.object({
20
+ unit: v.literal("none"),
21
+ })
22
+
23
+ export type NotionNoReminder = v.InferOutput<typeof notionNoReminderSchema>
24
+
25
+ /**
26
+ * Possible types for a Notion `datetime` reminder.
27
+ */
28
+ export const notionDateTimeReminderSchema = v.union([
29
+ notionDateReminderSchema,
30
+ notionTimeReminderSchema,
31
+ ])
32
+
33
+ export type NotionDateTimeReminder = v.InferOutput<
34
+ typeof notionDateTimeReminderSchema
35
+ >
36
+
37
+ const dateOrNoReminder = v.optional(
38
+ v.union([notionDateReminderSchema, notionNoReminderSchema]),
39
+ )
40
+
41
+ const dateTimeOrNoReminder = v.optional(
42
+ v.union([notionDateTimeReminderSchema, notionNoReminderSchema]),
43
+ )
44
+
45
+ export const notionDateSchema = v.object({
46
+ type: v.literal("date"),
47
+ start_date: v.string(),
48
+ reminder: dateOrNoReminder,
49
+ })
50
+ export type NotionDate = v.InferOutput<typeof notionDateSchema>
51
+
52
+ export const notionDateRangeSchema = v.object({
53
+ type: v.literal("daterange"),
54
+ start_date: v.string(),
55
+ end_date: v.string(),
56
+ reminder: dateOrNoReminder,
57
+ })
58
+ export type NotionDateRange = v.InferOutput<typeof notionDateRangeSchema>
59
+
60
+ export const notionDateTimeSchema = v.object({
61
+ type: v.literal("datetime"),
62
+ start_date: v.string(),
63
+ start_time: v.string(),
64
+ time_zone: v.string(),
65
+ reminder: dateTimeOrNoReminder,
66
+ })
67
+
68
+ export type NotionDateTime = v.InferOutput<typeof notionDateTimeSchema>
69
+
70
+ export const notionDateTimeRangeSchema = v.object({
71
+ type: v.literal("datetimerange"),
72
+ start_date: v.string(),
73
+ start_time: v.string(),
74
+ end_date: v.string(),
75
+ end_time: v.string(),
76
+ time_zone: v.string(),
77
+ reminder: dateTimeOrNoReminder,
78
+ })
79
+
80
+ export type NotionDateTimeRange = v.InferOutput<
81
+ typeof notionDateTimeRangeSchema
82
+ >
83
+
84
+ /**
85
+ * Possible types for a Notion `date` value.
86
+ */
87
+ export const notionDateValueSchema = v.variant("type", [
88
+ notionDateSchema,
89
+ notionDateRangeSchema,
90
+ notionDateTimeSchema,
91
+ notionDateTimeRangeSchema,
92
+ ])
93
+
94
+ export type NotionDateValue = Readonly<
95
+ v.InferOutput<typeof notionDateValueSchema>
96
+ >
@@ -0,0 +1,186 @@
1
+ import * as v from "valibot"
2
+
3
+ /**
4
+ * Hex-token identifiers for Notion's named colors. Mirrors the public API's
5
+ * `select.options[].color` enum
6
+ * (https://developers.notion.com/reference/property-object#select).
7
+ */
8
+ export const notionPropertyColorSchema = v.picklist([
9
+ "default",
10
+ "gray",
11
+ "brown",
12
+ "orange",
13
+ "yellow",
14
+ "green",
15
+ "blue",
16
+ "purple",
17
+ "pink",
18
+ "red",
19
+ "gray_background",
20
+ "brown_background",
21
+ "orange_background",
22
+ "yellow_background",
23
+ "green_background",
24
+ "blue_background",
25
+ "purple_background",
26
+ "pink_background",
27
+ "red_background",
28
+ "default_background",
29
+ ])
30
+
31
+ export type NotionPropertyColor = v.InferOutput<
32
+ typeof notionPropertyColorSchema
33
+ >
34
+
35
+ export const notionPropertyOptionSchema = v.object({
36
+ id: v.string(),
37
+ name: v.string(),
38
+ color: v.optional(notionPropertyColorSchema),
39
+ description: v.optional(v.string()),
40
+ })
41
+
42
+ export type NotionPropertyOption = v.InferOutput<
43
+ typeof notionPropertyOptionSchema
44
+ >
45
+
46
+ export const notionStatusGroupSchema = v.object({
47
+ id: v.string(),
48
+ name: v.string(),
49
+ color: v.optional(notionPropertyColorSchema),
50
+ option_ids: v.array(v.string()),
51
+ })
52
+
53
+ export type NotionStatusGroup = v.InferOutput<typeof notionStatusGroupSchema>
54
+
55
+ export const notionDualPropertySchema = v.object({
56
+ synced_property_id: v.string(),
57
+ synced_property_name: v.string(),
58
+ })
59
+
60
+ export type NotionDualProperty = v.InferOutput<typeof notionDualPropertySchema>
61
+
62
+ const baseProp = v.object({
63
+ name: v.string(),
64
+ description: v.optional(v.string()),
65
+ })
66
+
67
+ /**
68
+ * Every Notion property type the bridge speaks, in a single readable list.
69
+ * Mirrors the Notion public API
70
+ * [property object](https://developers.notion.com/reference/property-object)
71
+ * type field. Internal-only types (`button`, `verification`,
72
+ * `last_visited_time`, `location`) and the four built-ins (`created_time`,
73
+ * `last_edited_time`, `created_by`, `last_edited_by`) are included under their
74
+ * bridge-native names.
75
+ */
76
+ export const NOTION_PROPERTY_TYPES = [
77
+ "title",
78
+ "rich_text",
79
+ "number",
80
+ "checkbox",
81
+ "url",
82
+ "email",
83
+ "phone_number",
84
+ "select",
85
+ "multi_select",
86
+ "status",
87
+ "date",
88
+ "people",
89
+ "files",
90
+ "unique_id",
91
+ "relation",
92
+ "place",
93
+ "formula",
94
+ "rollup",
95
+ "button",
96
+ "verification",
97
+ "last_visited_time",
98
+ "location",
99
+ "created_time",
100
+ "last_edited_time",
101
+ "created_by",
102
+ "last_edited_by",
103
+ ] as const
104
+
105
+ export const notionPropertyTypeSchema = v.picklist(NOTION_PROPERTY_TYPES)
106
+
107
+ /**
108
+ * String literal union of every supported Notion property type. Same set as
109
+ * the discriminator in `NotionPropertySchema` — split out so the manifest
110
+ * can declare expected property types without dragging in the per-type
111
+ * payload shapes.
112
+ */
113
+ export type NotionPropertyType = v.InferOutput<typeof notionPropertyTypeSchema>
114
+
115
+ /**
116
+ * Per-property schema as exposed by the host over the custom-block bridge.
117
+ * The `type` discriminator must be one of {@link NOTION_PROPERTY_TYPES}.
118
+ */
119
+ export const notionPropertySchemaSchema = v.variant("type", [
120
+ v.object({ ...baseProp.entries, type: v.literal("title") }),
121
+ v.object({ ...baseProp.entries, type: v.literal("rich_text") }),
122
+ v.object({ ...baseProp.entries, type: v.literal("number") }),
123
+ v.object({ ...baseProp.entries, type: v.literal("checkbox") }),
124
+ v.object({ ...baseProp.entries, type: v.literal("url") }),
125
+ v.object({ ...baseProp.entries, type: v.literal("email") }),
126
+ v.object({ ...baseProp.entries, type: v.literal("phone_number") }),
127
+ v.object({
128
+ ...baseProp.entries,
129
+ type: v.literal("select"),
130
+ options: v.array(notionPropertyOptionSchema),
131
+ }),
132
+ v.object({
133
+ ...baseProp.entries,
134
+ type: v.literal("multi_select"),
135
+ options: v.array(notionPropertyOptionSchema),
136
+ }),
137
+ v.object({
138
+ ...baseProp.entries,
139
+ type: v.literal("status"),
140
+ options: v.array(notionPropertyOptionSchema),
141
+ groups: v.array(notionStatusGroupSchema),
142
+ }),
143
+ v.object({ ...baseProp.entries, type: v.literal("date") }),
144
+ v.object({ ...baseProp.entries, type: v.literal("people") }),
145
+ v.object({ ...baseProp.entries, type: v.literal("files") }),
146
+ v.object({ ...baseProp.entries, type: v.literal("unique_id") }),
147
+ v.object({
148
+ ...baseProp.entries,
149
+ type: v.literal("relation"),
150
+ data_source_id: v.optional(v.string()),
151
+ dual_property: v.optional(notionDualPropertySchema),
152
+ }),
153
+ v.object({ ...baseProp.entries, type: v.literal("place") }),
154
+ v.object({ ...baseProp.entries, type: v.literal("formula") }),
155
+ v.object({ ...baseProp.entries, type: v.literal("rollup") }),
156
+
157
+ // Internal-only types passed through under their bridge-native names.
158
+ v.object({ ...baseProp.entries, type: v.literal("button") }),
159
+ v.object({ ...baseProp.entries, type: v.literal("verification") }),
160
+ v.object({ ...baseProp.entries, type: v.literal("last_visited_time") }),
161
+ v.object({ ...baseProp.entries, type: v.literal("location") }),
162
+
163
+ // Synthetic built-ins. The host always emits one of each per data source.
164
+ v.object({ ...baseProp.entries, type: v.literal("created_time") }),
165
+ v.object({ ...baseProp.entries, type: v.literal("last_edited_time") }),
166
+ v.object({ ...baseProp.entries, type: v.literal("created_by") }),
167
+ v.object({ ...baseProp.entries, type: v.literal("last_edited_by") }),
168
+ ])
169
+
170
+ export type NotionPropertySchema = v.InferOutput<
171
+ typeof notionPropertySchemaSchema
172
+ >
173
+
174
+ /**
175
+ * The four synthetic built-in property IDs the host always includes in every
176
+ * data source's `propertySchemasById` and every row's `propertiesById`.
177
+ */
178
+ export const NOTION_BUILTIN_PROPERTY_IDS = [
179
+ "created_time",
180
+ "last_edited_time",
181
+ "created_by",
182
+ "last_edited_by",
183
+ ] as const
184
+
185
+ export type NotionBuiltinPropertyId =
186
+ (typeof NOTION_BUILTIN_PROPERTY_IDS)[number]
@@ -0,0 +1,13 @@
1
+ import * as v from "valibot"
2
+
3
+ /**
4
+ * A pointer to a record in a database.
5
+ */
6
+ export const notionRecordPointerSchema = v.object({
7
+ id: v.string(),
8
+ table: v.string(),
9
+ })
10
+
11
+ export type NotionRecordPointer = v.InferOutput<
12
+ typeof notionRecordPointerSchema
13
+ >
@@ -0,0 +1,96 @@
1
+ import type { CustomBlockManifest } from "../manifest"
2
+ import type { NotionDataSource, NotionDataSourceBindings } from "./dataSource"
3
+ import type { NotionPropertySchema } from "./propertySchema"
4
+
5
+ type ResolveDataSourcesArgs = {
6
+ manifest: CustomBlockManifest | null
7
+ dataSourceBindings: NotionDataSourceBindings
8
+ }
9
+
10
+ /**
11
+ * Builds the public {@link NotionDataSource} list the SDK exposes to consumers.
12
+ *
13
+ * Combines the host-supplied bindings (collection pointers + schemas) with the
14
+ * manifest's declared data-source keys. When the manifest names a property
15
+ * that isn't in `propertyIdsByKey`, falls back to {@link findPropertyIdByManifestProperty}
16
+ * so renames in Notion still resolve as long as the schema name matches.
17
+ */
18
+ export function resolveDataSources(
19
+ args: ResolveDataSourcesArgs,
20
+ ): NotionDataSource[] {
21
+ if (args.manifest === null) {
22
+ return Object.entries(args.dataSourceBindings).map(([key, binding]) => ({
23
+ key,
24
+ collectionPointer: binding.collectionPointer,
25
+ collectionSchema: binding.collectionSchema,
26
+ propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
27
+ propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
28
+ }))
29
+ }
30
+
31
+ return Object.entries(args.manifest.dataSources).map(
32
+ ([key, manifestDataSource]) => {
33
+ const binding = args.dataSourceBindings[key]
34
+ const propertySchemasById =
35
+ binding?.collectionSchema?.propertiesById ?? {}
36
+
37
+ const bindingPropertyIdsByKey = binding?.propertyIdsByKey ?? {}
38
+ const propertyIdsByKey: Record<string, string | undefined> = {}
39
+ const manifestProperties = Object.entries(
40
+ manifestDataSource.properties ?? {},
41
+ )
42
+ for (const [propertyKey, manifestProperty] of manifestProperties) {
43
+ const propertyId =
44
+ propertyKey in bindingPropertyIdsByKey
45
+ ? bindingPropertyIdsByKey[propertyKey]
46
+ : findPropertyIdByManifestProperty(propertySchemasById, {
47
+ key: propertyKey,
48
+ type: manifestProperty.type,
49
+ })
50
+ const propertySchema =
51
+ propertyId === undefined ? undefined : propertySchemasById[propertyId]
52
+ propertyIdsByKey[propertyKey] =
53
+ propertySchema?.type === manifestProperty.type
54
+ ? propertyId
55
+ : undefined
56
+ }
57
+
58
+ return {
59
+ key,
60
+ collectionPointer: binding?.collectionPointer,
61
+ collectionSchema: binding?.collectionSchema,
62
+ propertyIdsByKey,
63
+ propertySchemasById,
64
+ }
65
+ },
66
+ )
67
+ }
68
+
69
+ export function findPropertyIdByManifestProperty(
70
+ propertySchemasById: Record<string, NotionPropertySchema>,
71
+ manifestProperty: { key: string; type: NotionPropertySchema["type"] },
72
+ ): string | undefined {
73
+ // `ManifestProperty.name` is display copy for setup UI. Binding fallback is based
74
+ // on the stable semantic key so copy changes don't affect property resolution.
75
+ const propertySchemaForKey = propertySchemasById[manifestProperty.key]
76
+ if (propertySchemaForKey?.type === manifestProperty.type) {
77
+ return manifestProperty.key
78
+ }
79
+
80
+ const normalizedKey = normalizePropertyName(manifestProperty.key)
81
+ for (const [propertyId, propertySchema] of Object.entries(
82
+ propertySchemasById,
83
+ )) {
84
+ if (
85
+ propertySchema.type === manifestProperty.type &&
86
+ normalizePropertyName(propertySchema.name) === normalizedKey
87
+ ) {
88
+ return propertyId
89
+ }
90
+ }
91
+ return undefined
92
+ }
93
+
94
+ function normalizePropertyName(value: string): string {
95
+ return value.trim().toLowerCase()
96
+ }
@@ -0,0 +1,96 @@
1
+ import * as v from "valibot"
2
+ import type {
3
+ NotionPagePropertyInputMap,
4
+ NotionPagePropertyWriteMap,
5
+ } from "../pages/page"
6
+ import { notionPagePropertyValueSchema } from "../pages/page"
7
+ import type { NotionDataSource } from "./dataSource"
8
+
9
+ export type PropertyWriteMapResolutionResult =
10
+ | { status: "success"; properties: NotionPagePropertyWriteMap }
11
+ | { status: "error"; error: string }
12
+
13
+ /**
14
+ * Resolves a public SDK property write map into the ID-keyed bridge shape.
15
+ *
16
+ * Identifiers in `properties` may be raw property IDs or data-source property keys; the latter
17
+ * are looked up in the `dataSource`'s `propertyIdsByKey`. Each value is re-parsed through
18
+ * `notionPagePropertyValueSchema` with its `id` rewritten to the resolved property ID.
19
+ */
20
+ export function resolvePropertyWriteMapForDataSource(args: {
21
+ dataSource: NotionDataSource | undefined
22
+ properties: NotionPagePropertyInputMap
23
+ operationName: string
24
+ }): PropertyWriteMapResolutionResult {
25
+ const { dataSource, properties, operationName } = args
26
+
27
+ const resolvedProperties: NotionPagePropertyWriteMap = {}
28
+ for (const [identifier, value] of Object.entries(properties)) {
29
+ const propertyIdResult = resolvePropertyIdentifierForDataSource({
30
+ dataSource,
31
+ identifier,
32
+ operationName,
33
+ })
34
+ if (propertyIdResult.status === "error") {
35
+ return propertyIdResult
36
+ }
37
+
38
+ const propertyId = propertyIdResult.propertyId
39
+ if (
40
+ value.id !== undefined &&
41
+ value.id !== identifier &&
42
+ value.id !== propertyId
43
+ ) {
44
+ return {
45
+ status: "error",
46
+ error: `Property ${identifier} resolved to ${propertyId} but value id was ${value.id}.`,
47
+ }
48
+ }
49
+ if (resolvedProperties[propertyId] !== undefined) {
50
+ return {
51
+ status: "error",
52
+ error: `Cannot set property ${propertyId} more than once.`,
53
+ }
54
+ }
55
+
56
+ const parsedValue = v.safeParse(notionPagePropertyValueSchema, {
57
+ ...value,
58
+ id: propertyId,
59
+ })
60
+ if (!parsedValue.success) {
61
+ return {
62
+ status: "error",
63
+ error: `Invalid value for property ${identifier}.`,
64
+ }
65
+ }
66
+
67
+ resolvedProperties[propertyId] = parsedValue.output
68
+ }
69
+
70
+ return { status: "success", properties: resolvedProperties }
71
+ }
72
+
73
+ /**
74
+ * Treats matching data-source keys as aliases for property IDs; all other identifiers are raw IDs.
75
+ */
76
+ export function resolvePropertyIdentifierForDataSource(args: {
77
+ dataSource: NotionDataSource | undefined
78
+ identifier: string
79
+ operationName: string
80
+ }):
81
+ | { status: "success"; propertyId: string }
82
+ | { status: "error"; error: string } {
83
+ const { dataSource, identifier, operationName } = args
84
+ if (dataSource !== undefined && identifier in dataSource.propertyIdsByKey) {
85
+ const propertyId = dataSource.propertyIdsByKey[identifier]
86
+ if (propertyId === undefined) {
87
+ return {
88
+ status: "error",
89
+ error: `${operationName} cannot resolve property key "${identifier}" because it is not bound to a Notion property.`,
90
+ }
91
+ }
92
+ return { status: "success", propertyId }
93
+ }
94
+
95
+ return { status: "success", propertyId: identifier }
96
+ }