@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.
- package/AGENTS.md +27 -0
- package/README.md +86 -36
- package/dist/connections.d.ts +39 -11
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +85 -21
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/notion-as-code/database.d.ts +89 -48
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +132 -45
- package/dist/notion-as-code/database.test.d.ts +2 -0
- package/dist/notion-as-code/database.test.d.ts.map +1 -0
- package/dist/notion-as-code/handles.d.ts +2 -0
- package/dist/notion-as-code/handles.d.ts.map +1 -1
- package/dist/notion-as-code/handles.js +2 -0
- package/dist/notion-as-code/index.d.ts +5 -2
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +36 -3
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/notion-as-code/schema.d.ts +8 -4
- package/dist/notion-as-code/schema.d.ts.map +1 -1
- package/dist/notion-as-code/view.d.ts +12 -0
- package/dist/notion-as-code/view.d.ts.map +1 -0
- package/dist/notion-as-code/view.js +13 -0
- package/dist/notion-as-code/views-types.test.d.ts +2 -0
- package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
- package/dist/notion-as-code/views.d.ts +488 -0
- package/dist/notion-as-code/views.d.ts.map +1 -0
- package/dist/notion-as-code/views.js +0 -0
- package/dist/oauth.d.ts +11 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +26 -0
- package/dist/providers.generated.d.ts +26 -86
- package/dist/providers.generated.d.ts.map +1 -1
- package/dist/providers.generated.js +54 -66
- package/dist/sync.d.ts +33 -15
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +12 -0
- package/dist/triggers.generated.d.ts +3 -3
- package/dist/triggers.generated.d.ts.map +1 -1
- package/dist/workflow-state.d.ts +13 -0
- package/dist/workflow-state.d.ts.map +1 -0
- package/dist/workflow-state.js +107 -0
- package/dist/workflow.d.ts +56 -11
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +123 -10
- package/docs/BUILD.md +96 -0
- package/docs/CONNECTIONS.md +70 -22
- package/package.json +1 -1
- package/skills/connections/SKILL.md +7 -8
- package/skills/notion-as-code/SKILL.md +88 -50
- package/skills/sync/SKILL.md +21 -18
- package/skills/workflow/SKILL.md +52 -3
- package/src/cli/build.test.ts +124 -64
- package/src/connections.test.ts +205 -41
- package/src/connections.ts +144 -38
- package/src/index.ts +2 -0
- package/src/notion-as-code/database.test.ts +661 -0
- package/src/notion-as-code/database.ts +346 -129
- package/src/notion-as-code/handles.ts +2 -0
- package/src/notion-as-code/index.ts +71 -1
- package/src/notion-as-code/intents.ts +41 -4
- package/src/notion-as-code/schema.ts +11 -3
- package/src/notion-as-code/view.ts +23 -0
- package/src/notion-as-code/views-types.test.ts +59 -0
- package/src/notion-as-code/views.ts +573 -0
- package/src/oauth.ts +40 -0
- package/src/providers.generated.ts +68 -163
- package/src/sync.test.ts +295 -0
- package/src/sync.ts +85 -21
- package/src/triggers.generated.ts +4 -4
- package/src/workflow-connections-types.test.ts +16 -10
- package/src/workflow-state.ts +152 -0
- package/src/workflow-types.test.ts +88 -16
- package/src/workflow.test.ts +374 -18
- package/src/workflow.ts +211 -23
|
@@ -27,181 +27,86 @@ export const connections = {
|
|
|
27
27
|
outlook,
|
|
28
28
|
salesforce,
|
|
29
29
|
}
|
|
30
|
-
export type
|
|
31
|
-
calendar
|
|
32
|
-
slack
|
|
33
|
-
googleDriveOauth
|
|
34
|
-
discord
|
|
35
|
-
cursor
|
|
36
|
-
box
|
|
37
|
-
confluence
|
|
38
|
-
gmail
|
|
39
|
-
googleCalendar
|
|
40
|
-
googleDrive
|
|
41
|
-
outlook
|
|
42
|
-
salesforce
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return new SlackConnectionClient(invoker
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return new
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return new
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
91
|
-
|
|
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(
|
|
102
|
-
|
|
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(
|
|
109
|
-
|
|
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(
|
|
120
|
-
|
|
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(
|
|
131
|
-
|
|
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(
|
|
140
|
-
|
|
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(
|
|
147
|
-
|
|
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(
|
|
158
|
-
|
|
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(
|
|
165
|
-
|
|
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(
|
|
176
|
-
|
|
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(
|
|
187
|
-
|
|
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(
|
|
198
|
-
|
|
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,
|