@notionhq/apps 0.0.16 → 0.0.17

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 (56) hide show
  1. package/README.md +27 -2
  2. package/dist/connections.d.ts +39 -11
  3. package/dist/connections.d.ts.map +1 -1
  4. package/dist/connections.js +85 -21
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/notion-as-code/database.d.ts +3 -2
  9. package/dist/notion-as-code/database.d.ts.map +1 -1
  10. package/dist/notion-as-code/handles.d.ts +2 -0
  11. package/dist/notion-as-code/handles.d.ts.map +1 -1
  12. package/dist/notion-as-code/handles.js +2 -0
  13. package/dist/notion-as-code/index.d.ts +3 -0
  14. package/dist/notion-as-code/index.d.ts.map +1 -1
  15. package/dist/notion-as-code/intents.d.ts +2 -1
  16. package/dist/notion-as-code/intents.d.ts.map +1 -1
  17. package/dist/notion-as-code/view.d.ts +12 -0
  18. package/dist/notion-as-code/view.d.ts.map +1 -0
  19. package/dist/notion-as-code/view.js +13 -0
  20. package/dist/notion-as-code/views-types.test.d.ts +2 -0
  21. package/dist/notion-as-code/views-types.test.d.ts.map +1 -0
  22. package/dist/notion-as-code/views.d.ts +469 -0
  23. package/dist/notion-as-code/views.d.ts.map +1 -0
  24. package/dist/notion-as-code/views.js +0 -0
  25. package/dist/oauth.d.ts +11 -0
  26. package/dist/oauth.d.ts.map +1 -0
  27. package/dist/oauth.js +26 -0
  28. package/dist/providers.generated.d.ts +26 -86
  29. package/dist/providers.generated.d.ts.map +1 -1
  30. package/dist/providers.generated.js +54 -66
  31. package/dist/triggers.generated.d.ts +3 -3
  32. package/dist/triggers.generated.d.ts.map +1 -1
  33. package/dist/workflow.d.ts +8 -8
  34. package/dist/workflow.d.ts.map +1 -1
  35. package/dist/workflow.js +12 -6
  36. package/docs/CONNECTIONS.md +70 -22
  37. package/package.json +1 -1
  38. package/skills/connections/SKILL.md +7 -8
  39. package/skills/notion-as-code/SKILL.md +8 -2
  40. package/src/connections.test.ts +205 -41
  41. package/src/connections.ts +144 -38
  42. package/src/index.ts +1 -0
  43. package/src/notion-as-code/database.ts +3 -3
  44. package/src/notion-as-code/handles.ts +2 -0
  45. package/src/notion-as-code/index.ts +55 -0
  46. package/src/notion-as-code/intents.ts +2 -2
  47. package/src/notion-as-code/view.ts +23 -0
  48. package/src/notion-as-code/views-types.test.ts +59 -0
  49. package/src/notion-as-code/views.ts +554 -0
  50. package/src/oauth.ts +40 -0
  51. package/src/providers.generated.ts +68 -163
  52. package/src/triggers.generated.ts +4 -4
  53. package/src/workflow-connections-types.test.ts +16 -10
  54. package/src/workflow-types.test.ts +88 -16
  55. package/src/workflow.test.ts +45 -17
  56. package/src/workflow.ts +31 -17
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  // Generated by scripts/generate-triggers.ts — do not edit by hand.
2
2
  // Regenerate with: pnpm run generate
3
3
 
4
- import type { WorkflowConnection, WorkflowConnectionKeys } from "./connections.js";
4
+ import type { WorkflowConnectionDeclarations, WorkflowConnectionKeys } from "./connections.js";
5
5
 
6
6
  import type {
7
7
  CalendarEventCanceledEvent,
@@ -438,7 +438,7 @@ export const triggers = {
438
438
  } as const;
439
439
 
440
440
  /** Trigger creators restricted to the workflow's declared provider keys. */
441
- export type WorkflowTriggerCreators<TConnections extends readonly WorkflowConnection[]> = {
441
+ export type WorkflowTriggerCreators<TConnections extends WorkflowConnectionDeclarations> = {
442
442
  slackMessage(options?: { connectionKey: WorkflowConnectionKeys<TConnections, "slack"> }): SlackMessageTrigger<WorkflowConnectionKeys<TConnections, "slack">>;
443
443
  slackReactionAdded(options?: { connectionKey: WorkflowConnectionKeys<TConnections, "slack"> }): SlackReactionAddedTrigger<WorkflowConnectionKeys<TConnections, "slack">>;
444
444
  slackAppMention(options?: { connectionKey: WorkflowConnectionKeys<TConnections, "slack"> }): SlackAppMentionTrigger<WorkflowConnectionKeys<TConnections, "slack">>;
@@ -463,7 +463,7 @@ export type WorkflowTriggerCreators<TConnections extends readonly WorkflowConnec
463
463
  };
464
464
 
465
465
  /** Triggers whose connection keys belong to the declared provider. */
466
- export type WorkflowTriggerForConnections<TConnections extends readonly WorkflowConnection[]> =
466
+ export type WorkflowTriggerForConnections<TConnections extends WorkflowConnectionDeclarations> =
467
467
  | SlackMessageTrigger<WorkflowConnectionKeys<TConnections, "slack">>
468
468
  | SlackReactionAddedTrigger<WorkflowConnectionKeys<TConnections, "slack">>
469
469
  | SlackAppMentionTrigger<WorkflowConnectionKeys<TConnections, "slack">>
@@ -487,7 +487,7 @@ export type WorkflowTriggerForConnections<TConnections extends readonly Workflow
487
487
  | RecurrenceTrigger;
488
488
 
489
489
  /** @internal */
490
- export function createWorkflowTriggers<TConnections extends readonly WorkflowConnection[]>(): WorkflowTriggerCreators<TConnections> {
490
+ export function createWorkflowTriggers<TConnections extends WorkflowConnectionDeclarations>(): WorkflowTriggerCreators<TConnections> {
491
491
  return triggers;
492
492
  }
493
493
 
@@ -1,26 +1,32 @@
1
1
  import { expectTypeOf, it } from "vitest";
2
2
  import { triggers } from "./triggers.generated.js";
3
- import { connections, workflow, type WorkflowConnection } from "./workflow.js";
3
+ import { connections, workflow, type WorkflowConnectionDeclarations } from "./workflow.js";
4
4
 
5
5
  it("infers only the declared provider clients", () => {
6
6
  workflow({
7
7
  name: "Calendar",
8
8
  description: "Calendar only",
9
9
  triggers: [triggers.notionPageCreated()],
10
- connections: [connections.calendar({ key: "work" })],
10
+ connections: { work: connections.calendar() },
11
11
  handler(_event, context) {
12
- expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"calendar">();
13
- expectTypeOf(context.connections.calendar).toBeFunction();
12
+ expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"work">();
13
+ expectTypeOf(context.connections.work.listCalendars).toBeFunction();
14
14
  },
15
15
  });
16
- const requirements = [connections.calendar(), connections.slack({ key: "support" })];
16
+ const requirements = {
17
+ calendar: connections.calendar(),
18
+ support: connections.slack(),
19
+ team: connections.slack(),
20
+ };
17
21
  workflow({
18
22
  name: "Multiple providers",
19
- description: "Preserves providers in a separately declared array",
23
+ description: "Preserves clients in a separately declared object",
20
24
  triggers: [triggers.notionPageCreated()],
21
25
  connections: requirements,
22
26
  handler(_event, context) {
23
- expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"calendar" | "slack">();
27
+ expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<
28
+ "calendar" | "support" | "team"
29
+ >();
24
30
  },
25
31
  });
26
32
  });
@@ -38,7 +44,7 @@ it("exposes no providers for omitted or empty connections", () => {
38
44
  name: "No connections",
39
45
  description: "Empty",
40
46
  triggers: [triggers.notionPageCreated()],
41
- connections: [],
47
+ connections: {},
42
48
  handler(_event, context) {
43
49
  expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<never>();
44
50
  },
@@ -46,14 +52,14 @@ it("exposes no providers for omitted or empty connections", () => {
46
52
  });
47
53
 
48
54
  it("preserves literal declarations checked with satisfies", () => {
49
- const requirements = [connections.slack()] satisfies readonly WorkflowConnection[];
55
+ const requirements = { support: connections.slack() } satisfies WorkflowConnectionDeclarations;
50
56
  workflow({
51
57
  name: "Slack",
52
58
  description: "Slack only",
53
59
  triggers: [triggers.notionPageCreated()],
54
60
  connections: requirements,
55
61
  handler(_event, context) {
56
- expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"slack">();
62
+ expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"support">();
57
63
  },
58
64
  });
59
65
  });
@@ -7,44 +7,44 @@ it("checks provider keys on callback arguments and returned triggers", () => {
7
7
  {
8
8
  name: "valid",
9
9
  valid: true,
10
- connections: '[connections.slack({ key: "support" }), connections.calendar()]',
10
+ connections: "{ support: connections.slack(), calendar: connections.calendar() }",
11
11
  trigger:
12
12
  'triggers.slackMessage({ connectionKey: "support" }), triggers.calendarEventCreated({ connectionKey: "calendar" })',
13
13
  },
14
14
  {
15
15
  name: "default",
16
16
  valid: true,
17
- connections: "[connections.slack()]",
17
+ connections: "{ slack: connections.slack() }",
18
18
  trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
19
19
  },
20
20
  {
21
21
  name: "unbound",
22
22
  valid: true,
23
- connections: "[]",
23
+ connections: "{}",
24
24
  trigger: "triggers.slackMessage(), triggers.notionPageCreated()",
25
25
  },
26
26
  {
27
27
  name: "global_valid",
28
28
  valid: true,
29
- connections: '[connections.slack({ key: "support" })]',
29
+ connections: "{ support: connections.slack() }",
30
30
  trigger: 'globalTriggers.slackMessage({ connectionKey: "support" })',
31
31
  },
32
32
  {
33
33
  name: "undeclared",
34
34
  valid: false,
35
- connections: '[connections.slack({ key: "support" })]',
35
+ connections: "{ support: connections.slack() }",
36
36
  trigger: 'triggers.slackMessage({ connectionKey: "typo" })',
37
37
  },
38
38
  {
39
39
  name: "wrong_provider",
40
40
  valid: false,
41
- connections: '[connections.calendar({ key: "support" })]',
41
+ connections: "{ support: connections.calendar() }",
42
42
  trigger: 'triggers.slackMessage({ connectionKey: "support" })',
43
43
  },
44
44
  {
45
45
  name: "custom_not_default",
46
46
  valid: false,
47
- connections: '[connections.slack({ key: "support" })]',
47
+ connections: "{ support: connections.slack() }",
48
48
  trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
49
49
  },
50
50
  {
@@ -56,51 +56,123 @@ it("checks provider keys on callback arguments and returned triggers", () => {
56
56
  {
57
57
  name: "global_invalid",
58
58
  valid: false,
59
- connections: '[connections.calendar({ key: "support" })]',
59
+ connections: "{ support: connections.calendar() }",
60
60
  trigger: 'globalTriggers.slackMessage({ connectionKey: "support" })',
61
61
  },
62
62
  {
63
63
  name: "literal_invalid",
64
64
  valid: false,
65
- connections: '[connections.calendar({ key: "support" })]',
65
+ connections: "{ support: connections.calendar() }",
66
66
  trigger: '{ type: "slack.message", connectionKey: "support" }',
67
67
  },
68
68
  {
69
- name: "generic_without_key",
69
+ name: "old_key_option",
70
70
  valid: false,
71
- connections: '[connections.slack<"support">()]',
71
+ connections: '{ support: connections.slack({ key: "support" }) }',
72
72
  trigger: 'triggers.slackMessage({ connectionKey: "support" })',
73
73
  },
74
74
  {
75
75
  name: "literal_default",
76
76
  valid: true,
77
- connections: '[{ type: "slack" }]',
77
+ connections: '{ slack: { type: "slack" } }',
78
78
  trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
79
79
  },
80
80
  {
81
81
  name: "saved",
82
82
  valid: true,
83
- connections: "[support]",
83
+ connections: "{ support }",
84
84
  trigger: 'triggers.slackMessage({ connectionKey: "support" })',
85
85
  },
86
86
  {
87
87
  name: "saved_invalid",
88
88
  valid: false,
89
- connections: "[support]",
89
+ connections: "{ support }",
90
90
  trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
91
91
  },
92
+ {
93
+ name: "old_array",
94
+ valid: false,
95
+ connections: "[connections.slack()]",
96
+ trigger: "triggers.slackMessage()",
97
+ },
98
+ {
99
+ name: "two_slack",
100
+ valid: true,
101
+ connections: "{ support: connections.slack(), team: connections.slack() }",
102
+ trigger: 'triggers.slackMessage({ connectionKey: "team" })',
103
+ },
104
+
105
+ {
106
+ name: "keyed_clients",
107
+ valid: true,
108
+ connections:
109
+ "{ support: connections.slack(), team: connections.slack(), work: connections.calendar() }",
110
+ trigger: "triggers.notionPageCreated()",
111
+ handler:
112
+ '(_event, ctx) => { ctx.connections.support.findUserByEmail({ email: "a@example.com" }); ctx.connections.team.findUserByEmail({ email: "b@example.com" }); ctx.connections.work.listCalendars({}); }',
113
+ },
114
+ {
115
+ name: "wrong_client_method",
116
+ valid: false,
117
+ connections: "{ support: connections.slack() }",
118
+ trigger: "triggers.notionPageCreated()",
119
+ handler: "(_event, ctx) => { ctx.connections.support.listCalendars({}); }",
120
+ },
121
+ {
122
+ name: "undeclared_client",
123
+ valid: false,
124
+ connections: "{ support: connections.slack() }",
125
+ trigger: "triggers.notionPageCreated()",
126
+ handler:
127
+ '(_event, ctx) => { ctx.connections.slack.findUserByEmail({ email: "a@example.com" }); }',
128
+ },
129
+ {
130
+ name: "old_provider_call",
131
+ valid: false,
132
+ connections: "{ support: connections.slack() }",
133
+ trigger: "triggers.notionPageCreated()",
134
+ handler: '(_event, ctx) => { ctx.connections.support("support"); }',
135
+ },
136
+ {
137
+ name: "oauth_keyed_client",
138
+ valid: true,
139
+ connections: "{ github: connections.oauth(oauthConfig), support: connections.slack() }",
140
+ trigger: 'triggers.slackMessage({ connectionKey: "support" })',
141
+ handler:
142
+ "async (_event, ctx) => { const token: string = await ctx.connections.github.accessToken(); void token; }",
143
+ },
144
+ {
145
+ name: "oauth_wrong_trigger",
146
+ valid: false,
147
+ connections: "{ github: connections.oauth(oauthConfig) }",
148
+ trigger: 'triggers.slackMessage({ connectionKey: "github" })',
149
+ },
150
+ {
151
+ name: "oauth_old_key_option",
152
+ valid: false,
153
+ connections: '{ github: connections.oauth({ ...oauthConfig, key: "github" }) }',
154
+ trigger: "triggers.notionPageCreated()",
155
+ },
156
+ {
157
+ name: "oauth_old_provider_call",
158
+ valid: false,
159
+ connections: "{ github: connections.oauth(oauthConfig) }",
160
+ trigger: "triggers.notionPageCreated()",
161
+ handler: '(_event, ctx) => { ctx.connections.oauth("github"); }',
162
+ },
92
163
  ];
93
164
  const fixtures = new Map(
94
165
  cases.map((testCase) => [
95
166
  resolve("src", `__workflow_types_${testCase.name}.ts`),
96
167
  `import { connections, workflow } from "./workflow.js";
97
168
  import { triggers as globalTriggers } from "./triggers.generated.js";
98
- const support = connections.slack({ key: "support" });
169
+ const support = connections.slack();
170
+ const oauthConfig = { authorizationEndpoint: "https://example.com/authorize", tokenEndpoint: "https://example.com/token", clientId: "client", clientSecretEnv: "CLIENT_SECRET", scope: "read" };
99
171
  workflow({
100
172
  name: "Type check", description: "Type check",
101
173
  ${testCase.connections === undefined ? "" : `connections: ${testCase.connections},`}
102
174
  triggers: ({ triggers }) => [${testCase.trigger}],
103
- handler: () => {},
175
+ handler: ${testCase.handler ?? "() => {}"},
104
176
  });`,
105
177
  ]),
106
178
  );
@@ -37,8 +37,8 @@ afterEach(async () => {
37
37
 
38
38
  const event = { type: "notion.page.created" } as NotionPageCreatedEvent;
39
39
 
40
- async function createWorkflowContext(): Promise<WorkflowContext<readonly []>> {
41
- let context: WorkflowContext<readonly []> | undefined;
40
+ async function createWorkflowContext(): Promise<WorkflowContext<Record<never, never>>> {
41
+ let context: WorkflowContext<Record<never, never>> | undefined;
42
42
  const definition = workflow({
43
43
  name: "Test workflow",
44
44
  description: "Captures its workflow context",
@@ -743,23 +743,54 @@ describe("workflow", () => {
743
743
  name: "Schedule Follow-up",
744
744
  description: "Schedules a follow-up after a page is created",
745
745
  triggers: [triggers.notionPageCreated()],
746
- connections: [connections.calendar()],
746
+ connections: { calendar: connections.calendar() },
747
747
  handler: () => {},
748
748
  });
749
749
 
750
- expect(definition.config.connections).toEqual([{ type: "calendar" }]);
750
+ expect(definition.config.connections).toEqual([{ type: "calendar", key: "calendar" }]);
751
751
  });
752
752
 
753
- it("rejects duplicate connection requirements", () => {
753
+ it("keeps declaration keys stable after the input object changes", async () => {
754
+ const declarations = { support: connections.slack() };
755
+ const seen: string[][] = [];
756
+ const definition = workflow({
757
+ name: "Snapshot connections",
758
+ description: "Captures declared keys",
759
+ connections: declarations,
760
+ triggers: [triggers.notionPageCreated()],
761
+ handler(_event, context) {
762
+ seen.push(Object.keys(context.connections));
763
+ },
764
+ });
765
+ Object.assign(declarations, { support: connections.calendar(), team: connections.slack() });
766
+ expect(definition.config.connections).toEqual([{ type: "slack", key: "support" }]);
767
+ await definition.handler(event, { concreteOutput: true });
768
+ expect(seen).toEqual([["support"]]);
769
+ });
770
+ it("rejects inherited declaration objects before cloning", () => {
771
+ const declarations = { support: connections.slack() };
772
+ Object.setPrototypeOf(declarations, { extra: connections.calendar() });
773
+ expect(() =>
774
+ workflow({
775
+ name: "Inherited keys",
776
+ description: "Reject prototype declarations",
777
+ connections: declarations,
778
+ triggers: [triggers.notionPageCreated()],
779
+ handler() {},
780
+ }),
781
+ ).toThrow("only own");
782
+ });
783
+
784
+ it("rejects invalid connection property names", () => {
754
785
  expect(() =>
755
786
  workflow({
756
787
  name: "Schedule Follow-up",
757
788
  description: "Schedules a follow-up after a page is created",
758
789
  triggers: [triggers.notionPageCreated()],
759
- connections: [connections.calendar(), connections.calendar()],
790
+ connections: { "bad/key": connections.calendar() },
760
791
  handler: () => {},
761
792
  }),
762
- ).toThrowError("Duplicate workflow connection: calendar");
793
+ ).toThrowError("Invalid workflow connection key: bad/key");
763
794
  });
764
795
 
765
796
  it("writes a success envelope after the handler runs", async () => {
@@ -839,7 +870,7 @@ describe("connection-bound triggers", () => {
839
870
  const definition = workflow({
840
871
  name: "Typed triggers",
841
872
  description: "Uses only declared provider keys",
842
- connections: [connections.slack({ key: "support" }), connections.calendar()],
873
+ connections: { support: connections.slack(), calendar: connections.calendar() },
843
874
  triggers: ({ triggers }) => {
844
875
  calls += 1;
845
876
  expectTypeOf(triggers.slackMessage)
@@ -861,7 +892,7 @@ describe("connection-bound triggers", () => {
861
892
  "slack.message" | "calendar.event.created"
862
893
  >();
863
894
  expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<
864
- "slack" | "calendar"
895
+ "support" | "calendar"
865
896
  >();
866
897
  },
867
898
  });
@@ -876,7 +907,7 @@ describe("connection-bound triggers", () => {
876
907
  const definition = workflow({
877
908
  name: "Support",
878
909
  description: "Watch support messages",
879
- connections: [connections.slack({ key: "support" })],
910
+ connections: { support: connections.slack() },
880
911
  triggers: [triggers.slackMessage({ connectionKey: "support" })],
881
912
  handler(event) {
882
913
  expectTypeOf(event.type).toEqualTypeOf<"slack.message">();
@@ -887,9 +918,9 @@ describe("connection-bound triggers", () => {
887
918
  ]);
888
919
  });
889
920
  it.each([
890
- { requirements: [] },
891
- { requirements: [connections.calendar({ key: "support" })] },
892
- { requirements: [connections.slack({ key: "other" })] },
921
+ { requirements: {} },
922
+ { requirements: { support: connections.calendar() } },
923
+ { requirements: { other: connections.slack() } },
893
924
  ])("rejects missing or incompatible declarations: %j", ({ requirements }) => {
894
925
  expect(() =>
895
926
  workflow({
@@ -905,10 +936,7 @@ describe("connection-bound triggers", () => {
905
936
  const configuration = {
906
937
  name: "Support",
907
938
  description: "",
908
- connections: [
909
- connections.slack({ key: "support" }),
910
- connections.slack({ key: "internal" }),
911
- ],
939
+ connections: { support: connections.slack(), internal: connections.slack() },
912
940
  handler() {},
913
941
  };
914
942
  expect(() =>