@notionhq/apps 0.0.16 → 0.0.18

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 (77) hide show
  1. package/AGENTS.md +27 -0
  2. package/README.md +86 -36
  3. package/dist/connections.d.ts +39 -11
  4. package/dist/connections.d.ts.map +1 -1
  5. package/dist/connections.js +85 -21
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/notion-as-code/database.d.ts +89 -48
  10. package/dist/notion-as-code/database.d.ts.map +1 -1
  11. package/dist/notion-as-code/database.js +132 -45
  12. package/dist/notion-as-code/database.test.d.ts +2 -0
  13. package/dist/notion-as-code/database.test.d.ts.map +1 -0
  14. package/dist/notion-as-code/handles.d.ts +2 -0
  15. package/dist/notion-as-code/handles.d.ts.map +1 -1
  16. package/dist/notion-as-code/handles.js +2 -0
  17. package/dist/notion-as-code/index.d.ts +5 -2
  18. package/dist/notion-as-code/index.d.ts.map +1 -1
  19. package/dist/notion-as-code/intents.d.ts +36 -3
  20. package/dist/notion-as-code/intents.d.ts.map +1 -1
  21. package/dist/notion-as-code/schema.d.ts +8 -4
  22. package/dist/notion-as-code/schema.d.ts.map +1 -1
  23. package/dist/notion-as-code/view.d.ts +12 -0
  24. package/dist/notion-as-code/view.d.ts.map +1 -0
  25. package/dist/notion-as-code/view.js +13 -0
  26. package/dist/notion-as-code/views-types.test.d.ts +2 -0
  27. package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
  28. package/dist/notion-as-code/views.d.ts +488 -0
  29. package/dist/notion-as-code/views.d.ts.map +1 -0
  30. package/dist/notion-as-code/views.js +0 -0
  31. package/dist/oauth.d.ts +11 -0
  32. package/dist/oauth.d.ts.map +1 -0
  33. package/dist/oauth.js +26 -0
  34. package/dist/providers.generated.d.ts +26 -86
  35. package/dist/providers.generated.d.ts.map +1 -1
  36. package/dist/providers.generated.js +54 -66
  37. package/dist/sync.d.ts +33 -15
  38. package/dist/sync.d.ts.map +1 -1
  39. package/dist/sync.js +12 -0
  40. package/dist/triggers.generated.d.ts +3 -3
  41. package/dist/triggers.generated.d.ts.map +1 -1
  42. package/dist/workflow-state.d.ts +13 -0
  43. package/dist/workflow-state.d.ts.map +1 -0
  44. package/dist/workflow-state.js +107 -0
  45. package/dist/workflow.d.ts +56 -11
  46. package/dist/workflow.d.ts.map +1 -1
  47. package/dist/workflow.js +123 -10
  48. package/docs/BUILD.md +96 -0
  49. package/docs/CONNECTIONS.md +70 -22
  50. package/package.json +1 -1
  51. package/skills/connections/SKILL.md +7 -8
  52. package/skills/notion-as-code/SKILL.md +88 -50
  53. package/skills/sync/SKILL.md +21 -18
  54. package/skills/workflow/SKILL.md +52 -3
  55. package/src/cli/build.test.ts +124 -64
  56. package/src/connections.test.ts +205 -41
  57. package/src/connections.ts +144 -38
  58. package/src/index.ts +2 -0
  59. package/src/notion-as-code/database.test.ts +661 -0
  60. package/src/notion-as-code/database.ts +346 -129
  61. package/src/notion-as-code/handles.ts +2 -0
  62. package/src/notion-as-code/index.ts +71 -1
  63. package/src/notion-as-code/intents.ts +41 -4
  64. package/src/notion-as-code/schema.ts +11 -3
  65. package/src/notion-as-code/view.ts +23 -0
  66. package/src/notion-as-code/views-types.test.ts +59 -0
  67. package/src/notion-as-code/views.ts +573 -0
  68. package/src/oauth.ts +40 -0
  69. package/src/providers.generated.ts +68 -163
  70. package/src/sync.test.ts +295 -0
  71. package/src/sync.ts +85 -21
  72. package/src/triggers.generated.ts +4 -4
  73. package/src/workflow-connections-types.test.ts +16 -10
  74. package/src/workflow-state.ts +152 -0
  75. package/src/workflow-types.test.ts +88 -16
  76. package/src/workflow.test.ts +374 -18
  77. package/src/workflow.ts +211 -23
@@ -27,181 +27,86 @@ export const connections = {
27
27
  outlook,
28
28
  salesforce,
29
29
  }
30
- export type ConnectionProviders = {
31
- calendar(key?: string): CalendarConnectionClient
32
- slack(key?: string): SlackConnectionClient
33
- googleDriveOauth(key?: string): GoogleDriveOauthConnectionClient
34
- discord(key?: string): DiscordConnectionClient
35
- cursor(key?: string): CursorConnectionClient
36
- box(key?: string): BoxConnectionClient
37
- confluence(key?: string): ConfluenceConnectionClient
38
- gmail(key?: string): GmailConnectionClient
39
- googleCalendar(key?: string): GoogleCalendarConnectionClient
40
- googleDrive(key?: string): GoogleDriveConnectionClient
41
- outlook(key?: string): OutlookConnectionClient
42
- salesforce(key?: string): SalesforceConnectionClient
30
+ export type ConnectionClients = {
31
+ calendar: CalendarConnectionClient
32
+ slack: SlackConnectionClient
33
+ googleDriveOauth: GoogleDriveOauthConnectionClient
34
+ discord: DiscordConnectionClient
35
+ cursor: CursorConnectionClient
36
+ box: BoxConnectionClient
37
+ confluence: ConfluenceConnectionClient
38
+ gmail: GmailConnectionClient
39
+ googleCalendar: GoogleCalendarConnectionClient
40
+ googleDrive: GoogleDriveConnectionClient
41
+ outlook: OutlookConnectionClient
42
+ salesforce: SalesforceConnectionClient
43
43
  }
44
- export function createConnectionProviders(
45
- invoker: (provider: string, key: string) => ConnectionActionInvoker,
46
- ): ConnectionProviders {
47
- return {
48
- calendar(key = "calendar"): CalendarConnectionClient {
49
- return new CalendarConnectionClient(invoker("calendar", key))
50
- },
51
- slack(key = "slack"): SlackConnectionClient {
52
- return new SlackConnectionClient(invoker("slack", key))
53
- },
54
- googleDriveOauth(
55
- key = "googleDriveOauth",
56
- ): GoogleDriveOauthConnectionClient {
57
- return new GoogleDriveOauthConnectionClient(
58
- invoker("googleDriveOauth", key),
59
- )
60
- },
61
- discord(key = "discord"): DiscordConnectionClient {
62
- return new DiscordConnectionClient(invoker("discord", key))
63
- },
64
- cursor(key = "cursor"): CursorConnectionClient {
65
- return new CursorConnectionClient(invoker("cursor", key))
66
- },
67
- box(key = "box"): BoxConnectionClient {
68
- return new BoxConnectionClient(invoker("box", key))
69
- },
70
- confluence(key = "confluence"): ConfluenceConnectionClient {
71
- return new ConfluenceConnectionClient(invoker("confluence", key))
72
- },
73
- gmail(key = "gmail"): GmailConnectionClient {
74
- return new GmailConnectionClient(invoker("gmail", key))
75
- },
76
- googleCalendar(key = "googleCalendar"): GoogleCalendarConnectionClient {
77
- return new GoogleCalendarConnectionClient(invoker("googleCalendar", key))
78
- },
79
- googleDrive(key = "googleDrive"): GoogleDriveConnectionClient {
80
- return new GoogleDriveConnectionClient(invoker("googleDrive", key))
81
- },
82
- outlook(key = "outlook"): OutlookConnectionClient {
83
- return new OutlookConnectionClient(invoker("outlook", key))
84
- },
85
- salesforce(key = "salesforce"): SalesforceConnectionClient {
86
- return new SalesforceConnectionClient(invoker("salesforce", key))
87
- },
44
+ export function createConnectionClient(
45
+ provider: string,
46
+ invoker: ConnectionActionInvoker,
47
+ ): ConnectionClients[keyof ConnectionClients] {
48
+ switch (provider) {
49
+ case "calendar":
50
+ return new CalendarConnectionClient(invoker)
51
+ case "slack":
52
+ return new SlackConnectionClient(invoker)
53
+ case "googleDriveOauth":
54
+ return new GoogleDriveOauthConnectionClient(invoker)
55
+ case "discord":
56
+ return new DiscordConnectionClient(invoker)
57
+ case "cursor":
58
+ return new CursorConnectionClient(invoker)
59
+ case "box":
60
+ return new BoxConnectionClient(invoker)
61
+ case "confluence":
62
+ return new ConfluenceConnectionClient(invoker)
63
+ case "gmail":
64
+ return new GmailConnectionClient(invoker)
65
+ case "googleCalendar":
66
+ return new GoogleCalendarConnectionClient(invoker)
67
+ case "googleDrive":
68
+ return new GoogleDriveConnectionClient(invoker)
69
+ case "outlook":
70
+ return new OutlookConnectionClient(invoker)
71
+ case "salesforce":
72
+ return new SalesforceConnectionClient(invoker)
73
+ default:
74
+ throw new Error("Unsupported connection provider: " + provider)
88
75
  }
89
76
  }
90
- function calendar(options?: {
91
- key?: never
92
- }): WorkflowConnection<"calendar", "calendar">
93
- function calendar<const TKey extends string>(options: {
94
- key: TKey
95
- }): WorkflowConnection<"calendar", TKey>
96
- function calendar(
97
- options: { key?: string } = {},
98
- ): WorkflowConnection<"calendar"> {
99
- return { type: "calendar", ...options }
77
+ function calendar(): WorkflowConnection<"calendar"> {
78
+ return { type: "calendar" }
100
79
  }
101
- function slack(options?: { key?: never }): WorkflowConnection<"slack", "slack">
102
- function slack<const TKey extends string>(options: {
103
- key: TKey
104
- }): WorkflowConnection<"slack", TKey>
105
- function slack(options: { key?: string } = {}): WorkflowConnection<"slack"> {
106
- return { type: "slack", ...options }
80
+ function slack(): WorkflowConnection<"slack"> {
81
+ return { type: "slack" }
107
82
  }
108
- function googleDriveOauth(options?: {
109
- key?: never
110
- }): WorkflowConnection<"googleDriveOauth", "googleDriveOauth">
111
- function googleDriveOauth<const TKey extends string>(options: {
112
- key: TKey
113
- }): WorkflowConnection<"googleDriveOauth", TKey>
114
- function googleDriveOauth(
115
- options: { key?: string } = {},
116
- ): WorkflowConnection<"googleDriveOauth"> {
117
- return { type: "googleDriveOauth", ...options }
83
+ function googleDriveOauth(): WorkflowConnection<"googleDriveOauth"> {
84
+ return { type: "googleDriveOauth" }
118
85
  }
119
- function discord(options?: {
120
- key?: never
121
- }): WorkflowConnection<"discord", "discord">
122
- function discord<const TKey extends string>(options: {
123
- key: TKey
124
- }): WorkflowConnection<"discord", TKey>
125
- function discord(
126
- options: { key?: string } = {},
127
- ): WorkflowConnection<"discord"> {
128
- return { type: "discord", ...options }
86
+ function discord(): WorkflowConnection<"discord"> {
87
+ return { type: "discord" }
129
88
  }
130
- function cursor(options?: {
131
- key?: never
132
- }): WorkflowConnection<"cursor", "cursor">
133
- function cursor<const TKey extends string>(options: {
134
- key: TKey
135
- }): WorkflowConnection<"cursor", TKey>
136
- function cursor(options: { key?: string } = {}): WorkflowConnection<"cursor"> {
137
- return { type: "cursor", ...options }
89
+ function cursor(): WorkflowConnection<"cursor"> {
90
+ return { type: "cursor" }
138
91
  }
139
- function box(options?: { key?: never }): WorkflowConnection<"box", "box">
140
- function box<const TKey extends string>(options: {
141
- key: TKey
142
- }): WorkflowConnection<"box", TKey>
143
- function box(options: { key?: string } = {}): WorkflowConnection<"box"> {
144
- return { type: "box", ...options }
92
+ function box(): WorkflowConnection<"box"> {
93
+ return { type: "box" }
145
94
  }
146
- function confluence(options?: {
147
- key?: never
148
- }): WorkflowConnection<"confluence", "confluence">
149
- function confluence<const TKey extends string>(options: {
150
- key: TKey
151
- }): WorkflowConnection<"confluence", TKey>
152
- function confluence(
153
- options: { key?: string } = {},
154
- ): WorkflowConnection<"confluence"> {
155
- return { type: "confluence", ...options }
95
+ function confluence(): WorkflowConnection<"confluence"> {
96
+ return { type: "confluence" }
156
97
  }
157
- function gmail(options?: { key?: never }): WorkflowConnection<"gmail", "gmail">
158
- function gmail<const TKey extends string>(options: {
159
- key: TKey
160
- }): WorkflowConnection<"gmail", TKey>
161
- function gmail(options: { key?: string } = {}): WorkflowConnection<"gmail"> {
162
- return { type: "gmail", ...options }
98
+ function gmail(): WorkflowConnection<"gmail"> {
99
+ return { type: "gmail" }
163
100
  }
164
- function googleCalendar(options?: {
165
- key?: never
166
- }): WorkflowConnection<"googleCalendar", "googleCalendar">
167
- function googleCalendar<const TKey extends string>(options: {
168
- key: TKey
169
- }): WorkflowConnection<"googleCalendar", TKey>
170
- function googleCalendar(
171
- options: { key?: string } = {},
172
- ): WorkflowConnection<"googleCalendar"> {
173
- return { type: "googleCalendar", ...options }
101
+ function googleCalendar(): WorkflowConnection<"googleCalendar"> {
102
+ return { type: "googleCalendar" }
174
103
  }
175
- function googleDrive(options?: {
176
- key?: never
177
- }): WorkflowConnection<"googleDrive", "googleDrive">
178
- function googleDrive<const TKey extends string>(options: {
179
- key: TKey
180
- }): WorkflowConnection<"googleDrive", TKey>
181
- function googleDrive(
182
- options: { key?: string } = {},
183
- ): WorkflowConnection<"googleDrive"> {
184
- return { type: "googleDrive", ...options }
104
+ function googleDrive(): WorkflowConnection<"googleDrive"> {
105
+ return { type: "googleDrive" }
185
106
  }
186
- function outlook(options?: {
187
- key?: never
188
- }): WorkflowConnection<"outlook", "outlook">
189
- function outlook<const TKey extends string>(options: {
190
- key: TKey
191
- }): WorkflowConnection<"outlook", TKey>
192
- function outlook(
193
- options: { key?: string } = {},
194
- ): WorkflowConnection<"outlook"> {
195
- return { type: "outlook", ...options }
107
+ function outlook(): WorkflowConnection<"outlook"> {
108
+ return { type: "outlook" }
196
109
  }
197
- function salesforce(options?: {
198
- key?: never
199
- }): WorkflowConnection<"salesforce", "salesforce">
200
- function salesforce<const TKey extends string>(options: {
201
- key: TKey
202
- }): WorkflowConnection<"salesforce", TKey>
203
- function salesforce(
204
- options: { key?: string } = {},
205
- ): WorkflowConnection<"salesforce"> {
206
- return { type: "salesforce", ...options }
110
+ function salesforce(): WorkflowConnection<"salesforce"> {
111
+ return { type: "salesforce" }
207
112
  }
package/src/sync.test.ts CHANGED
@@ -36,6 +36,20 @@ const keyTypes = createDatabase("key-types", {
36
36
  Project: Schema.relation(projects),
37
37
  },
38
38
  });
39
+ const partialMeetings = createDatabase("partial-meetings", {
40
+ schema: {
41
+ Name: Schema.title(),
42
+ "Meeting ID": Schema.richText(),
43
+ Other: Schema.richText(),
44
+ "Meeting date": Schema.date(),
45
+ "Prep complete": Schema.checkbox(),
46
+ },
47
+ });
48
+ const keyOnly = createDatabase("key-only", {
49
+ schema: {
50
+ ID: Schema.richText(),
51
+ },
52
+ });
39
53
 
40
54
  type IssueKey = KeyPropertyName<typeof issues.config.schema>;
41
55
  const textKey: IssueKey = "GitHub ID";
@@ -128,6 +142,188 @@ describe("createSync", () => {
128
142
  ).toBe("GitHub ID");
129
143
  });
130
144
 
145
+ it("accepts partial upsert properties in standalone handlers", () => {
146
+ const finiteProperties = {
147
+ Name: Builder.title("Prepared"),
148
+ Unknown: Builder.text("unexpected"),
149
+ };
150
+ const spreadProperties = {
151
+ Unknown: Builder.text("unexpected from spread"),
152
+ };
153
+
154
+ createSync({
155
+ database: partialMeetings,
156
+ primaryKey: "Meeting ID",
157
+ handler: async (state: { cursor: number } | undefined) => ({
158
+ changes: [
159
+ {
160
+ type: "upsert",
161
+ key: "meeting-1",
162
+ properties: { Name: Builder.title("One") },
163
+ },
164
+ {
165
+ type: "upsert",
166
+ key: "meeting-2",
167
+ properties: {
168
+ "Meeting date": Builder.date("2026-09-16"),
169
+ "Prep complete": Builder.checkbox(true),
170
+ },
171
+ },
172
+ { type: "delete", key: "meeting-3" },
173
+ ],
174
+ nextState: { cursor: (state?.cursor ?? 0) + 1 },
175
+ }),
176
+ });
177
+
178
+ const invalidSyncDeclarations = () => {
179
+ createSync({
180
+ database: partialMeetings,
181
+ primaryKey: "Meeting ID",
182
+ // @ts-expect-error Unknown property keys are rejected in inferred handler returns.
183
+ handler: async () => ({
184
+ changes: [
185
+ {
186
+ type: "upsert",
187
+ key: "invalid-inline",
188
+ properties: {
189
+ Name: Builder.title("Known"),
190
+ Unknown: Builder.text("unexpected"),
191
+ },
192
+ },
193
+ ],
194
+ }),
195
+ });
196
+ createSync({
197
+ database: partialMeetings,
198
+ primaryKey: "Meeting ID",
199
+ // @ts-expect-error Unknown properties remain rejected when another change shares the key.
200
+ handler: async () => ({
201
+ changes: [
202
+ {
203
+ type: "upsert",
204
+ key: "same",
205
+ properties: {
206
+ Name: Builder.title("Known"),
207
+ Unknown: Builder.text("unexpected"),
208
+ },
209
+ },
210
+ {
211
+ type: "upsert",
212
+ key: "same",
213
+ properties: { Name: Builder.title("Known sibling") },
214
+ },
215
+ ],
216
+ }),
217
+ });
218
+ createSync({
219
+ database: partialMeetings,
220
+ primaryKey: "Meeting ID",
221
+ // @ts-expect-error Upsert properties must include a non-primary-key schema property.
222
+ handler: async () => ({
223
+ changes: [
224
+ {
225
+ type: "upsert",
226
+ key: "invalid-empty",
227
+ properties: {},
228
+ },
229
+ ],
230
+ }),
231
+ });
232
+ createSync({
233
+ database: partialMeetings,
234
+ primaryKey: "Meeting ID",
235
+ // @ts-expect-error Finite property objects cannot contain unknown keys.
236
+ handler: async () => ({
237
+ changes: [
238
+ {
239
+ type: "upsert",
240
+ key: "invalid-finite",
241
+ properties: finiteProperties,
242
+ },
243
+ ],
244
+ }),
245
+ });
246
+ createSync({
247
+ database: partialMeetings,
248
+ primaryKey: "Meeting ID",
249
+ // @ts-expect-error Spread property objects cannot contain unknown keys.
250
+ handler: async () => ({
251
+ changes: [
252
+ {
253
+ type: "upsert",
254
+ key: "invalid-spread",
255
+ properties: {
256
+ Name: Builder.title("Known from spread"),
257
+ ...spreadProperties,
258
+ },
259
+ },
260
+ ],
261
+ }),
262
+ });
263
+ createSync({
264
+ database: partialMeetings,
265
+ primaryKey: "Meeting ID",
266
+ // @ts-expect-error Known properties must use their declared value types.
267
+ handler: async () => ({
268
+ changes: [
269
+ {
270
+ type: "upsert",
271
+ key: "invalid-value",
272
+ properties: {
273
+ Name: Builder.people("person@example.com"),
274
+ },
275
+ },
276
+ ],
277
+ }),
278
+ });
279
+ createSync({
280
+ database: partialMeetings,
281
+ primaryKey: "Meeting ID",
282
+ // @ts-expect-error The primary-key property is supplied from change.key.
283
+ handler: async () => ({
284
+ changes: [
285
+ {
286
+ type: "upsert",
287
+ key: "invalid-primary-property",
288
+ properties: {
289
+ "Meeting ID": Builder.text("explicit"),
290
+ },
291
+ },
292
+ ],
293
+ }),
294
+ });
295
+ createSync({
296
+ database: partialMeetings,
297
+ // @ts-expect-error Unknown schema properties cannot be sync primary keys.
298
+ primaryKey: "Unknown",
299
+ handler: async () => ({ changes: [] }),
300
+ });
301
+ };
302
+ void invalidSyncDeclarations;
303
+ });
304
+
305
+ it("rejects key-only upserts but allows deletes", () => {
306
+ const invalidKeyOnlyUpsert = () => {
307
+ createSync({
308
+ database: keyOnly,
309
+ primaryKey: "ID",
310
+ // @ts-expect-error Upserts require at least one non-primary-key schema property.
311
+ handler: async () => ({
312
+ changes: [{ type: "upsert", key: "invalid-key-only", properties: {} }],
313
+ }),
314
+ });
315
+ };
316
+ void invalidKeyOnlyUpsert;
317
+
318
+ createSync({
319
+ database: keyOnly,
320
+ primaryKey: "ID",
321
+ handler: async () => ({
322
+ changes: [{ type: "delete", key: "deleted-key-only" }],
323
+ }),
324
+ });
325
+ });
326
+
131
327
  it("rejects invalid schedules when the sync is created", () => {
132
328
  expect(() =>
133
329
  createSync({
@@ -177,6 +373,105 @@ describe("createSync", () => {
177
373
  ]);
178
374
  });
179
375
 
376
+ it("preserves omitted properties, partial upserts, and deletes at runtime", async () => {
377
+ const sync = createSync({
378
+ database: partialMeetings,
379
+ primaryKey: "Meeting ID",
380
+ handler: async (state: { cursor: number } | undefined) => ({
381
+ changes: [
382
+ {
383
+ type: "upsert",
384
+ key: "meeting-1",
385
+ properties: { Name: Builder.title("One") },
386
+ },
387
+ {
388
+ type: "upsert",
389
+ key: "meeting-2",
390
+ properties: { Other: Builder.text("Only other") },
391
+ },
392
+ {
393
+ type: "upsert",
394
+ key: "meeting-3",
395
+ properties: {
396
+ "Meeting date": Builder.date("2026-09-16"),
397
+ "Prep complete": Builder.checkbox(true),
398
+ },
399
+ },
400
+ { type: "delete", key: "meeting-4" },
401
+ ],
402
+ nextState: { cursor: (state?.cursor ?? 0) + 1 },
403
+ }),
404
+ });
405
+
406
+ const result = await sync.handler({ state: { cursor: 4 } }, { concreteOutput: true });
407
+
408
+ expect(result).toEqual({
409
+ changes: [
410
+ {
411
+ type: "upsert",
412
+ key: "meeting-1",
413
+ properties: {
414
+ Name: [["One"]],
415
+ "Meeting ID": [["meeting-1"]],
416
+ },
417
+ targetDatabaseKey: "partial-meetings",
418
+ },
419
+ {
420
+ type: "upsert",
421
+ key: "meeting-2",
422
+ properties: {
423
+ Other: [["Only other"]],
424
+ "Meeting ID": [["meeting-2"]],
425
+ },
426
+ targetDatabaseKey: "partial-meetings",
427
+ },
428
+ {
429
+ type: "upsert",
430
+ key: "meeting-3",
431
+ properties: {
432
+ "Meeting date": [
433
+ ["‣", [["d", { type: "date", start_date: "2026-09-16" }]]],
434
+ ],
435
+ "Prep complete": [["Yes"]],
436
+ "Meeting ID": [["meeting-3"]],
437
+ },
438
+ targetDatabaseKey: "partial-meetings",
439
+ },
440
+ {
441
+ type: "delete",
442
+ key: "meeting-4",
443
+ targetDatabaseKey: "partial-meetings",
444
+ },
445
+ ],
446
+ hasMore: false,
447
+ nextUserContext: { cursor: 5 },
448
+ nextPacerStates: {},
449
+ });
450
+ });
451
+
452
+ it("rejects empty upserts at runtime", async () => {
453
+ const sync = createSync({
454
+ database: partialMeetings,
455
+ primaryKey: "Meeting ID",
456
+ handler: async () => ({
457
+ changes: [
458
+ {
459
+ type: "upsert",
460
+ key: "meeting-empty",
461
+ properties: {},
462
+ icon: Builder.notionIcon("warning"),
463
+ cover: Builder.imageCover("https://example.com/empty-cover.png"),
464
+ pageContentMarkdown: "# Empty properties",
465
+ },
466
+ ],
467
+ }),
468
+ } as never);
469
+
470
+ await expect(sync.handler(undefined, { concreteOutput: true })).rejects.toBeInstanceOf(
471
+ ExecutionError,
472
+ );
473
+ });
474
+
180
475
  it("passes deletes, paging state, and incremental mode through", async () => {
181
476
  const sync = createSync({
182
477
  database: issues,