@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
package/src/sync.ts CHANGED
@@ -3,8 +3,8 @@ import { createCapabilityContext } from "./context.js";
3
3
  import type { Database } from "./database.js";
4
4
  import { ExecutionError, RateLimitError } from "./error.js";
5
5
  import type { DataSourceHandle } from "./notion-as-code/database.js";
6
- import type { NotionAsCodeProperty } from "./notion-as-code/intents.js";
7
- import type { AppsSchemaForProperties } from "./notion-as-code/schema.js";
6
+ import type { NotionAsCodeSchema } from "./notion-as-code/intents.js";
7
+ import type { AppsSchemaForSchema } from "./notion-as-code/schema.js";
8
8
  import { writeOutput } from "./output.js";
9
9
  import { initPacerState, pacerStates, type PacerEntry } from "./pacer.js";
10
10
  import { resolveRuntimeInput } from "./runtime-input.js";
@@ -45,12 +45,50 @@ type PropertyValue<T extends PropertyConfiguration> = T extends { type: "people"
45
45
  ? RelationValue
46
46
  : TextValue;
47
47
 
48
+ type SyncPropertyName<S extends DatabaseSchema> = Extract<keyof S, string>;
49
+
50
+ /**
51
+ * Collect unknown property names across the whole inferred change union before checking it.
52
+ * Otherwise a valid subset properties object could structurally hide an invalid sibling row.
53
+ */
54
+ type SyncUnknownPropertyNames<AllowedPropertyNames extends string, Change> = Change extends {
55
+ type: "upsert";
56
+ properties: infer Properties extends object;
57
+ }
58
+ ? Exclude<keyof Properties, AllowedPropertyNames>
59
+ : never;
60
+
61
+ /** Require at least one key while keeping all other keys optional. */
62
+ type AtLeastOne<T extends object> = Partial<T> &
63
+ {
64
+ [RequiredKey in keyof T]-?: Required<Pick<T, RequiredKey>>;
65
+ }[keyof T];
66
+
67
+ type SyncUpsertProperties<
68
+ S extends DatabaseSchema,
69
+ PrimaryKey extends KeyPropertyName<S>,
70
+ > = AtLeastOne<{
71
+ [P in Exclude<SyncPropertyName<S>, PrimaryKey>]: PropertyValue<S[P]>;
72
+ }>;
73
+
74
+ type ExactSyncHandlerResult<
75
+ AllowedPropertyNames extends string,
76
+ Result extends { changes: readonly unknown[] },
77
+ > = Result & {
78
+ changes: [SyncUnknownPropertyNames<AllowedPropertyNames, Result["changes"][number]>] extends [
79
+ never,
80
+ ]
81
+ ? Result["changes"]
82
+ : never;
83
+ };
84
+
48
85
  export type SyncMode = "replace" | "incremental";
49
86
 
87
+ /** Upsert one or more known non-primary-key properties; the key is supplied separately. */
50
88
  export type SyncChangeUpsert<S extends DatabaseSchema, PrimaryKey extends KeyPropertyName<S>> = {
51
89
  type: "upsert";
52
90
  key: string;
53
- properties: Omit<{ [P in keyof S]: PropertyValue<S[P]> }, PrimaryKey>;
91
+ properties: SyncUpsertProperties<S, PrimaryKey>;
54
92
  upstreamUpdatedAt?: string;
55
93
  icon?: Icon;
56
94
  cover?: Cover;
@@ -77,6 +115,11 @@ export type SyncConfiguration<
77
115
  S extends DatabaseSchema,
78
116
  PrimaryKey extends KeyPropertyName<S>,
79
117
  State,
118
+ Result extends SyncHandlerResult<S, PrimaryKey, State> = SyncHandlerResult<
119
+ S,
120
+ PrimaryKey,
121
+ State
122
+ >,
80
123
  > = {
81
124
  database: Database<S>;
82
125
  primaryKey: PrimaryKey;
@@ -85,25 +128,25 @@ export type SyncConfiguration<
85
128
  handler: (
86
129
  state: State | undefined,
87
130
  context: CapabilityContext,
88
- ) => Promise<SyncHandlerResult<NoInfer<S>, NoInfer<PrimaryKey>, State>>;
131
+ ) => Promise<
132
+ // NoInfer prevents handler output from widening the source schema or primary-key inference.
133
+ SyncHandlerResult<NoInfer<S>, NoInfer<PrimaryKey>, State> &
134
+ ExactSyncHandlerResult<
135
+ Exclude<SyncPropertyName<NoInfer<S>>, NoInfer<PrimaryKey>>,
136
+ Result
137
+ >
138
+ >;
89
139
  };
90
140
 
91
141
  /** Configuration for a sync backed by a Notion-as-Code data source. */
92
142
  export type DataSourceSyncConfiguration<
93
- Properties extends readonly NotionAsCodeProperty[],
94
- PrimaryKey extends KeyPropertyName<AppsSchemaForProperties<Properties>>,
143
+ Schema extends NotionAsCodeSchema,
144
+ PrimaryKey extends KeyPropertyName<AppsSchemaForSchema<Schema>>,
95
145
  State,
96
- > = {
97
- dataSource: DataSourceHandle<Properties>;
98
- primaryKey: PrimaryKey;
99
- mode?: SyncMode;
100
- schedule?: Schedule;
101
- handler: (
102
- state: State | undefined,
103
- context: CapabilityContext,
104
- ) => Promise<
105
- SyncHandlerResult<NoInfer<AppsSchemaForProperties<Properties>>, NoInfer<PrimaryKey>, State>
106
- >;
146
+ Result extends SyncHandlerResult<AppsSchemaForSchema<Schema>, PrimaryKey, State> =
147
+ SyncHandlerResult<AppsSchemaForSchema<Schema>, PrimaryKey, State>,
148
+ > = Omit<SyncConfiguration<AppsSchemaForSchema<Schema>, PrimaryKey, State, Result>, "database"> & {
149
+ dataSource: DataSourceHandle<Schema>;
107
150
  };
108
151
 
109
152
  export type SyncRuntimeContext = {
@@ -160,16 +203,23 @@ export function createSync<
160
203
  const S extends DatabaseSchema,
161
204
  const PrimaryKey extends KeyPropertyName<S>,
162
205
  State = unknown,
163
- >(configuration: SyncConfiguration<S, PrimaryKey, State>): Sync {
206
+ const Result extends SyncHandlerResult<S, PrimaryKey, State> = SyncHandlerResult<
207
+ S,
208
+ PrimaryKey,
209
+ State
210
+ >,
211
+ >(configuration: SyncConfiguration<S, PrimaryKey, State, Result>): Sync {
164
212
  return createSyncDefinition(configuration);
165
213
  }
166
214
 
167
215
  /** Create a sync backed by a Notion-as-Code data source. */
168
216
  export function sync<
169
- const Properties extends readonly NotionAsCodeProperty[],
170
- const PrimaryKey extends KeyPropertyName<AppsSchemaForProperties<Properties>>,
217
+ const Schema extends NotionAsCodeSchema,
218
+ const PrimaryKey extends KeyPropertyName<AppsSchemaForSchema<Schema>>,
171
219
  State = unknown,
172
- >(configuration: DataSourceSyncConfiguration<Properties, PrimaryKey, State>): Sync {
220
+ const Result extends SyncHandlerResult<AppsSchemaForSchema<Schema>, PrimaryKey, State> =
221
+ SyncHandlerResult<AppsSchemaForSchema<Schema>, PrimaryKey, State>,
222
+ >(configuration: DataSourceSyncConfiguration<Schema, PrimaryKey, State, Result>): Sync {
173
223
  return createSyncDefinition(configuration);
174
224
  }
175
225
 
@@ -199,6 +249,20 @@ function createSyncDefinition<State>(configuration: SyncDefinitionConfiguration<
199
249
  if (change.type === "delete") {
200
250
  return { ...change, targetDatabaseKey: database.key };
201
251
  }
252
+
253
+ let hasOwnEnumerableProperty = false;
254
+ for (const propertyName in change.properties) {
255
+ if (Object.hasOwn(change.properties, propertyName)) {
256
+ hasOwnEnumerableProperty = true;
257
+ break;
258
+ }
259
+ }
260
+ if (!hasOwnEnumerableProperty) {
261
+ throw new Error(
262
+ "Sync upsert properties must include at least one property.",
263
+ );
264
+ }
265
+
202
266
  return {
203
267
  ...change,
204
268
  properties: {
@@ -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
  });
@@ -0,0 +1,152 @@
1
+ import type { RunMetadata } from "./runtime-metadata.js";
2
+
3
+ export type WorkflowStateValue =
4
+ | null
5
+ | boolean
6
+ | number
7
+ | string
8
+ | ReadonlyArray<WorkflowStateValue>
9
+ | { readonly [key: string]: WorkflowStateValue };
10
+
11
+ export type WorkflowState = {
12
+ get(key: string): Promise<WorkflowStateValue | undefined>;
13
+ set(key: string, value: WorkflowStateValue | undefined): void;
14
+ };
15
+
16
+ type WorkflowStateMutation =
17
+ | { operation: "set"; key: string; value: WorkflowStateValue }
18
+ | { operation: "delete"; key: string };
19
+
20
+ type WorkflowStepScope = {
21
+ id: string;
22
+ active: boolean;
23
+ mutations: Map<string, WorkflowStateMutation>;
24
+ };
25
+
26
+ const MAX_STATE_KEY_BYTES = 256;
27
+ const WORKFLOW_CAPABILITY_KEY_ENV = "NOTION_WORKFLOW_CAPABILITY_KEY";
28
+
29
+ export function createWorkflowStepState(
30
+ metadata: Pick<RunMetadata, "workerId">,
31
+ stepId: string,
32
+ ): {
33
+ state: WorkflowState;
34
+ run<T>(fn: () => T | Promise<T>): Promise<T>;
35
+ } {
36
+ const scope: WorkflowStepScope = {
37
+ id: stepId,
38
+ active: true,
39
+ mutations: new Map(),
40
+ };
41
+ const state: WorkflowState = {
42
+ async get(key: string): Promise<WorkflowStateValue | undefined> {
43
+ requireActiveStep(scope, "get");
44
+ validateStateKey(key);
45
+ const pending = scope.mutations.get(key);
46
+ if (pending?.operation === "set") {
47
+ return pending.value;
48
+ }
49
+ if (pending?.operation === "delete") {
50
+ return undefined;
51
+ }
52
+
53
+ const response = await requestWorkflowState(metadata, {
54
+ operation: "get",
55
+ key,
56
+ });
57
+ const body: { exists: boolean; value?: WorkflowStateValue } = JSON.parse(
58
+ await response.text(),
59
+ );
60
+ if (!body.exists) {
61
+ return undefined;
62
+ }
63
+ return body.value;
64
+ },
65
+ set(key: string, value: WorkflowStateValue | undefined): void {
66
+ requireActiveStep(scope, "set");
67
+ validateStateKey(key);
68
+ if (value === undefined) {
69
+ recordMutation(scope, { operation: "delete", key });
70
+ return;
71
+ }
72
+ recordMutation(scope, { operation: "set", key, value });
73
+ },
74
+ };
75
+
76
+ return {
77
+ state,
78
+ async run<T>(fn: () => T | Promise<T>): Promise<T> {
79
+ try {
80
+ const result = await fn();
81
+ if (scope.mutations.size > 0) {
82
+ await requestWorkflowState(metadata, {
83
+ operation: "commit",
84
+ operationId: scope.id,
85
+ mutations: [...scope.mutations.values()],
86
+ });
87
+ }
88
+ return result;
89
+ } finally {
90
+ scope.active = false;
91
+ }
92
+ },
93
+ };
94
+ }
95
+
96
+ function requireActiveStep(expectedScope: WorkflowStepScope, operation: keyof WorkflowState): void {
97
+ if (!expectedScope.active) {
98
+ throw new Error(`state.${operation}() must be called inside context.step().`);
99
+ }
100
+ }
101
+
102
+ function recordMutation(scope: WorkflowStepScope, mutation: WorkflowStateMutation): void {
103
+ scope.mutations.set(mutation.key, mutation);
104
+ }
105
+
106
+ async function requestWorkflowState(
107
+ metadata: Pick<RunMetadata, "workerId">,
108
+ request:
109
+ | { operation: "get"; key: string }
110
+ | {
111
+ operation: "commit";
112
+ operationId: string;
113
+ mutations: WorkflowStateMutation[];
114
+ },
115
+ ): Promise<Response> {
116
+ const baseUrl = process.env.NOTION_API_BASE_URL;
117
+ const token = process.env.NOTION_API_TOKEN;
118
+ const capabilityKey = process.env[WORKFLOW_CAPABILITY_KEY_ENV];
119
+ if (!baseUrl || !token || !capabilityKey) {
120
+ throw new Error(
121
+ `Workflow state is only available when the workflow runtime provides NOTION_API_BASE_URL, NOTION_API_TOKEN, and ${WORKFLOW_CAPABILITY_KEY_ENV}.`,
122
+ );
123
+ }
124
+
125
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v3/workersWorkflowState`, {
126
+ method: "POST",
127
+ headers: {
128
+ authorization: `Bearer ${token}`,
129
+ "content-type": "application/json",
130
+ },
131
+ body: JSON.stringify({
132
+ ...request,
133
+ workerId: metadata.workerId,
134
+ capabilityKey,
135
+ }),
136
+ });
137
+ if (!response.ok) {
138
+ throw new Error(
139
+ `Workflow state ${request.operation} failed with status ${response.status}.`,
140
+ );
141
+ }
142
+ return response;
143
+ }
144
+
145
+ function validateStateKey(key: string): void {
146
+ if (key.length === 0) {
147
+ throw new Error("Workflow state keys must not be empty.");
148
+ }
149
+ if (Buffer.byteLength(key) > MAX_STATE_KEY_BYTES) {
150
+ throw new Error(`Workflow state keys must be at most ${MAX_STATE_KEY_BYTES} UTF-8 bytes.`);
151
+ }
152
+ }
@@ -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
  );