@notionhq/apps 0.0.13 → 0.0.15

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 (111) hide show
  1. package/README.md +110 -49
  2. package/dist/box.generated.d.ts +164 -0
  3. package/dist/box.generated.d.ts.map +1 -0
  4. package/dist/box.generated.js +24 -0
  5. package/dist/calendar.generated.d.ts +1036 -0
  6. package/dist/calendar.generated.d.ts.map +1 -0
  7. package/dist/calendar.generated.js +42 -0
  8. package/dist/cli/build.js +7 -6
  9. package/dist/cli/emit-manifest.d.ts.map +1 -1
  10. package/dist/cli/emit-manifest.js +1 -4
  11. package/dist/confluence.generated.d.ts +98 -0
  12. package/dist/confluence.generated.d.ts.map +1 -0
  13. package/dist/confluence.generated.js +18 -0
  14. package/dist/connection-actions.d.ts +2 -0
  15. package/dist/connection-actions.d.ts.map +1 -0
  16. package/dist/connection-actions.js +0 -0
  17. package/dist/connection-trigger-definitions.generated.d.ts +34 -0
  18. package/dist/connection-trigger-definitions.generated.d.ts.map +1 -0
  19. package/dist/connection-trigger-definitions.generated.js +45 -0
  20. package/dist/connections.d.ts +15 -0
  21. package/dist/connections.d.ts.map +1 -0
  22. package/dist/connections.js +79 -0
  23. package/dist/connections.test.d.ts +2 -0
  24. package/dist/connections.test.d.ts.map +1 -0
  25. package/dist/context.d.ts.map +1 -1
  26. package/dist/context.js +3 -0
  27. package/dist/context.test.d.ts +2 -0
  28. package/dist/context.test.d.ts.map +1 -0
  29. package/dist/cursor.generated.d.ts +154 -0
  30. package/dist/cursor.generated.d.ts.map +1 -0
  31. package/dist/cursor.generated.js +18 -0
  32. package/dist/custom-block.d.ts +1 -1
  33. package/dist/custom-block.d.ts.map +1 -1
  34. package/dist/custom-block.js +3 -3
  35. package/dist/discord.generated.d.ts +279 -0
  36. package/dist/discord.generated.d.ts.map +1 -0
  37. package/dist/discord.generated.js +33 -0
  38. package/dist/gmail.generated.d.ts +184 -0
  39. package/dist/gmail.generated.d.ts.map +1 -0
  40. package/dist/gmail.generated.js +21 -0
  41. package/dist/googleCalendar.generated.d.ts +83 -0
  42. package/dist/googleCalendar.generated.d.ts.map +1 -0
  43. package/dist/googleCalendar.generated.js +15 -0
  44. package/dist/googleDrive.generated.d.ts +210 -0
  45. package/dist/googleDrive.generated.d.ts.map +1 -0
  46. package/dist/googleDrive.generated.js +27 -0
  47. package/dist/googleDriveOauth.generated.d.ts +851 -0
  48. package/dist/googleDriveOauth.generated.d.ts.map +1 -0
  49. package/dist/googleDriveOauth.generated.js +84 -0
  50. package/dist/index.d.ts +8 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +16 -0
  53. package/dist/outlook.generated.d.ts +224 -0
  54. package/dist/outlook.generated.d.ts.map +1 -0
  55. package/dist/outlook.generated.js +24 -0
  56. package/dist/providers.generated.d.ts +117 -0
  57. package/dist/providers.generated.d.ts.map +1 -0
  58. package/dist/providers.generated.js +108 -0
  59. package/dist/salesforce.generated.d.ts +151 -0
  60. package/dist/salesforce.generated.d.ts.map +1 -0
  61. package/dist/salesforce.generated.js +24 -0
  62. package/dist/slack.generated.d.ts +290 -0
  63. package/dist/slack.generated.d.ts.map +1 -0
  64. package/dist/slack.generated.js +30 -0
  65. package/dist/sync.d.ts +2 -2
  66. package/dist/sync.d.ts.map +1 -1
  67. package/dist/sync.js +3 -3
  68. package/dist/triggers.generated.d.ts +107 -32
  69. package/dist/triggers.generated.d.ts.map +1 -1
  70. package/dist/triggers.generated.js +36 -24
  71. package/dist/workflow-connections-types.test.d.ts +2 -0
  72. package/dist/workflow-connections-types.test.d.ts.map +1 -0
  73. package/dist/workflow-types.test.d.ts +2 -0
  74. package/dist/workflow-types.test.d.ts.map +1 -0
  75. package/dist/workflow.d.ts +24 -20
  76. package/dist/workflow.d.ts.map +1 -1
  77. package/dist/workflow.js +16 -19
  78. package/docs/BUILD.md +83 -14
  79. package/docs/CONNECTIONS.md +130 -0
  80. package/package.json +5 -1
  81. package/src/box.generated.ts +187 -0
  82. package/src/calendar.generated.ts +1117 -0
  83. package/src/cli/build.test.ts +35 -4
  84. package/src/cli/build.ts +6 -5
  85. package/src/cli/emit-manifest.ts +1 -5
  86. package/src/confluence.generated.ts +109 -0
  87. package/src/connection-actions.ts +2 -0
  88. package/src/connection-trigger-definitions.generated.ts +48 -0
  89. package/src/connections.test.ts +157 -0
  90. package/src/connections.ts +126 -0
  91. package/src/context.test.ts +26 -0
  92. package/src/context.ts +8 -0
  93. package/src/cursor.generated.ts +167 -0
  94. package/src/custom-block.test.ts +4 -4
  95. package/src/custom-block.ts +2 -2
  96. package/src/discord.generated.ts +318 -0
  97. package/src/gmail.generated.ts +205 -0
  98. package/src/googleCalendar.generated.ts +95 -0
  99. package/src/googleDrive.generated.ts +235 -0
  100. package/src/googleDriveOauth.generated.ts +980 -0
  101. package/src/index.ts +7 -0
  102. package/src/outlook.generated.ts +249 -0
  103. package/src/providers.generated.ts +207 -0
  104. package/src/salesforce.generated.ts +170 -0
  105. package/src/slack.generated.ts +330 -0
  106. package/src/sync.ts +2 -2
  107. package/src/triggers.generated.ts +112 -32
  108. package/src/workflow-connections-types.test.ts +59 -0
  109. package/src/workflow-types.test.ts +144 -0
  110. package/src/workflow.test.ts +135 -25
  111. package/src/workflow.ts +63 -34
@@ -0,0 +1,144 @@
1
+ import { resolve } from "node:path";
2
+ import ts from "typescript";
3
+ import { expect, it } from "vitest";
4
+
5
+ it("checks provider keys on callback arguments and returned triggers", () => {
6
+ const cases = [
7
+ {
8
+ name: "valid",
9
+ valid: true,
10
+ connections: '[connections.slack({ key: "support" }), connections.calendar()]',
11
+ trigger:
12
+ 'triggers.slackMessage({ connectionKey: "support" }), triggers.calendarEventCreated({ connectionKey: "calendar" })',
13
+ },
14
+ {
15
+ name: "default",
16
+ valid: true,
17
+ connections: "[connections.slack()]",
18
+ trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
19
+ },
20
+ {
21
+ name: "unbound",
22
+ valid: true,
23
+ connections: "[]",
24
+ trigger: "triggers.slackMessage(), triggers.notionPageCreated()",
25
+ },
26
+ {
27
+ name: "global_valid",
28
+ valid: true,
29
+ connections: '[connections.slack({ key: "support" })]',
30
+ trigger: 'globalTriggers.slackMessage({ connectionKey: "support" })',
31
+ },
32
+ {
33
+ name: "undeclared",
34
+ valid: false,
35
+ connections: '[connections.slack({ key: "support" })]',
36
+ trigger: 'triggers.slackMessage({ connectionKey: "typo" })',
37
+ },
38
+ {
39
+ name: "wrong_provider",
40
+ valid: false,
41
+ connections: '[connections.calendar({ key: "support" })]',
42
+ trigger: 'triggers.slackMessage({ connectionKey: "support" })',
43
+ },
44
+ {
45
+ name: "custom_not_default",
46
+ valid: false,
47
+ connections: '[connections.slack({ key: "support" })]',
48
+ trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
49
+ },
50
+ {
51
+ name: "missing",
52
+ valid: false,
53
+ connections: undefined,
54
+ trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
55
+ },
56
+ {
57
+ name: "global_invalid",
58
+ valid: false,
59
+ connections: '[connections.calendar({ key: "support" })]',
60
+ trigger: 'globalTriggers.slackMessage({ connectionKey: "support" })',
61
+ },
62
+ {
63
+ name: "literal_invalid",
64
+ valid: false,
65
+ connections: '[connections.calendar({ key: "support" })]',
66
+ trigger: '{ type: "slack.message", connectionKey: "support" }',
67
+ },
68
+ {
69
+ name: "generic_without_key",
70
+ valid: false,
71
+ connections: '[connections.slack<"support">()]',
72
+ trigger: 'triggers.slackMessage({ connectionKey: "support" })',
73
+ },
74
+ {
75
+ name: "literal_default",
76
+ valid: true,
77
+ connections: '[{ type: "slack" }]',
78
+ trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
79
+ },
80
+ {
81
+ name: "saved",
82
+ valid: true,
83
+ connections: "[support]",
84
+ trigger: 'triggers.slackMessage({ connectionKey: "support" })',
85
+ },
86
+ {
87
+ name: "saved_invalid",
88
+ valid: false,
89
+ connections: "[support]",
90
+ trigger: 'triggers.slackMessage({ connectionKey: "slack" })',
91
+ },
92
+ ];
93
+ const fixtures = new Map(
94
+ cases.map((testCase) => [
95
+ resolve("src", `__workflow_types_${testCase.name}.ts`),
96
+ `import { connections, workflow } from "./workflow.js";
97
+ import { triggers as globalTriggers } from "./triggers.generated.js";
98
+ const support = connections.slack({ key: "support" });
99
+ workflow({
100
+ name: "Type check", description: "Type check",
101
+ ${testCase.connections === undefined ? "" : `connections: ${testCase.connections},`}
102
+ triggers: ({ triggers }) => [${testCase.trigger}],
103
+ handler: () => {},
104
+ });`,
105
+ ]),
106
+ );
107
+ const config = ts.readConfigFile(resolve("tsconfig.json"), ts.sys.readFile);
108
+ expect(config.error).toBeUndefined();
109
+ const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, process.cwd());
110
+ expect(parsed.errors).toEqual([]);
111
+ const host = ts.createCompilerHost(parsed.options);
112
+ const getSourceFile = host.getSourceFile.bind(host);
113
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
114
+ const fixture = fixtures.get(fileName);
115
+ return fixture === undefined
116
+ ? getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile)
117
+ : ts.createSourceFile(fileName, fixture, languageVersion, true);
118
+ };
119
+ const program = ts.createProgram(
120
+ [...fixtures.keys()],
121
+ { ...parsed.options, noEmit: true },
122
+ host,
123
+ );
124
+ const diagnostics = ts.getPreEmitDiagnostics(program);
125
+ const unrelated = diagnostics.filter(
126
+ (diagnostic) => !diagnostic.file || !fixtures.has(diagnostic.file.fileName),
127
+ );
128
+ expect(
129
+ unrelated.map((diagnostic) =>
130
+ ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
131
+ ),
132
+ ).toEqual([]);
133
+ for (const testCase of cases) {
134
+ const errors = diagnostics.filter(
135
+ (diagnostic) =>
136
+ diagnostic.file?.fileName ===
137
+ resolve("src", `__workflow_types_${testCase.name}.ts`),
138
+ );
139
+ expect(
140
+ errors.length === 0,
141
+ `${testCase.name}: ${errors.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")).join("\n")}`,
142
+ ).toBe(testCase.valid);
143
+ }
144
+ }, 30_000);
@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi, type Moc
8
8
  import { ExecutionError } from "./error.js";
9
9
  import type { RunMetadata } from "./runtime-metadata.js";
10
10
  import { triggers, type NotionPageCreatedEvent } from "./triggers.generated.js";
11
- import { connections, createWorkflow, type StepContext, type WorkflowContext } from "./workflow.js";
11
+ import { connections, workflow, type StepContext, type WorkflowContext } from "./workflow.js";
12
12
 
13
13
  let stdoutSpy: Mock<typeof process.stdout.write>;
14
14
  let checkpointDirectory: string | undefined;
@@ -37,9 +37,9 @@ afterEach(async () => {
37
37
 
38
38
  const event = { type: "notion.page.created" } as NotionPageCreatedEvent;
39
39
 
40
- async function createWorkflowContext(): Promise<WorkflowContext> {
41
- let context: WorkflowContext | undefined;
42
- const workflow = createWorkflow({
40
+ async function createWorkflowContext(): Promise<WorkflowContext<readonly []>> {
41
+ let context: WorkflowContext<readonly []> | undefined;
42
+ const definition = workflow({
43
43
  name: "Test workflow",
44
44
  description: "Captures its workflow context",
45
45
  triggers: [triggers.notionPageCreated()],
@@ -48,7 +48,7 @@ async function createWorkflowContext(): Promise<WorkflowContext> {
48
48
  },
49
49
  });
50
50
 
51
- await workflow.handler(event, { concreteOutput: true });
51
+ await definition.handler(event, { concreteOutput: true });
52
52
 
53
53
  if (!context) {
54
54
  throw new Error("Workflow context was not created");
@@ -638,7 +638,7 @@ describe("step", () => {
638
638
 
639
639
  it("resets duplicate key tracking for each workflow call", async () => {
640
640
  const keys: string[] = [];
641
- const workflow = createWorkflow({
641
+ const definition = workflow({
642
642
  name: "Reuse step key",
643
643
  description: "Checks that a key can be reused in a new invocation",
644
644
  triggers: [triggers.notionPageCreated()],
@@ -654,14 +654,14 @@ describe("step", () => {
654
654
  },
655
655
  });
656
656
 
657
- await workflow.handler(event, { concreteOutput: true });
658
- await workflow.handler(event, { concreteOutput: true });
657
+ await definition.handler(event, { concreteOutput: true });
658
+ await definition.handler(event, { concreteOutput: true });
659
659
 
660
660
  expect(keys).toEqual([workflowStepKey(["First"]), workflowStepKey(["First"])]);
661
661
  });
662
662
 
663
663
  it("writes step events under concreteOutput", async () => {
664
- const workflow = createWorkflow({
664
+ const definition = workflow({
665
665
  name: "Log Pages",
666
666
  description: "Logs created pages",
667
667
  triggers: [triggers.notionPageCreated()],
@@ -670,7 +670,7 @@ describe("step", () => {
670
670
  },
671
671
  });
672
672
 
673
- await workflow.handler(event, { concreteOutput: true });
673
+ await definition.handler(event, { concreteOutput: true });
674
674
 
675
675
  expect(stdoutSpy).toHaveBeenCalledTimes(3);
676
676
  expect(stdoutSpy.mock.calls[0]?.[0]).toEqual(
@@ -721,17 +721,17 @@ async function writeRunMetadata(overrides: Partial<RunMetadata> = {}): Promise<v
721
721
  process.env.NOTION_RUN_METADATA_PATH = metadataPath;
722
722
  }
723
723
 
724
- describe("createWorkflow", () => {
724
+ describe("workflow", () => {
725
725
  it("is tagged and exposes triggers in config", () => {
726
- const workflow = createWorkflow({
726
+ const definition = workflow({
727
727
  name: "Log Pages",
728
728
  description: "Logs created pages",
729
729
  triggers: [triggers.notionPageCreated()],
730
730
  handler: () => {},
731
731
  });
732
732
 
733
- expect(workflow._tag).toBe("workflow");
734
- expect(workflow.config).toEqual({
733
+ expect(definition._tag).toBe("workflow");
734
+ expect(definition.config).toEqual({
735
735
  name: "Log Pages",
736
736
  description: "Logs created pages",
737
737
  triggers: [{ type: "notion.page.created" }],
@@ -739,7 +739,7 @@ describe("createWorkflow", () => {
739
739
  });
740
740
 
741
741
  it("declares a Calendar connection requirement", () => {
742
- const workflow = createWorkflow({
742
+ const definition = workflow({
743
743
  name: "Schedule Follow-up",
744
744
  description: "Schedules a follow-up after a page is created",
745
745
  triggers: [triggers.notionPageCreated()],
@@ -747,12 +747,12 @@ describe("createWorkflow", () => {
747
747
  handler: () => {},
748
748
  });
749
749
 
750
- expect(workflow.config.connections).toEqual([{ type: "calendar" }]);
750
+ expect(definition.config.connections).toEqual([{ type: "calendar" }]);
751
751
  });
752
752
 
753
753
  it("rejects duplicate connection requirements", () => {
754
754
  expect(() =>
755
- createWorkflow({
755
+ workflow({
756
756
  name: "Schedule Follow-up",
757
757
  description: "Schedules a follow-up after a page is created",
758
758
  triggers: [triggers.notionPageCreated()],
@@ -764,14 +764,14 @@ describe("createWorkflow", () => {
764
764
 
765
765
  it("writes a success envelope after the handler runs", async () => {
766
766
  const handler = vi.fn();
767
- const workflow = createWorkflow({
767
+ const definition = workflow({
768
768
  name: "Log Pages",
769
769
  description: "Logs created pages",
770
770
  triggers: [triggers.notionPageCreated()],
771
771
  handler,
772
772
  });
773
773
 
774
- await workflow.handler(event);
774
+ await definition.handler(event);
775
775
 
776
776
  expect(handler).toHaveBeenCalledWith(event, expect.anything());
777
777
  expect(stdoutSpy).toHaveBeenCalledWith(
@@ -784,7 +784,7 @@ describe("createWorkflow", () => {
784
784
  throw new Error("Metadata directory was not created");
785
785
  }
786
786
  const handler = vi.fn();
787
- const workflow = createWorkflow({
787
+ const definition = workflow({
788
788
  name: "Log Pages",
789
789
  description: "Logs created pages",
790
790
  triggers: [triggers.notionPageCreated()],
@@ -795,27 +795,27 @@ describe("createWorkflow", () => {
795
795
  await writeFile(inputPath, JSON.stringify(fileEvent));
796
796
  process.env.NOTION_INPUT_PAYLOAD_PATH = inputPath;
797
797
 
798
- await workflow.handler(event, { concreteOutput: true });
798
+ await definition.handler(event, { concreteOutput: true });
799
799
 
800
800
  expect(handler).toHaveBeenCalledWith(fileEvent, expect.anything());
801
801
  });
802
802
 
803
803
  it("returns the status without an envelope under concreteOutput", async () => {
804
- const workflow = createWorkflow({
804
+ const definition = workflow({
805
805
  name: "Log Pages",
806
806
  description: "Logs created pages",
807
807
  triggers: [triggers.notionPageCreated()],
808
808
  handler: () => {},
809
809
  });
810
810
 
811
- await expect(workflow.handler(event, { concreteOutput: true })).resolves.toEqual({
811
+ await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
812
812
  status: "success",
813
813
  });
814
814
  expect(stdoutSpy).not.toHaveBeenCalled();
815
815
  });
816
816
 
817
817
  it("writes an error envelope and throws ExecutionError on failure", async () => {
818
- const workflow = createWorkflow({
818
+ const definition = workflow({
819
819
  name: "Log Pages",
820
820
  description: "Logs created pages",
821
821
  triggers: [triggers.notionPageCreated()],
@@ -824,7 +824,7 @@ describe("createWorkflow", () => {
824
824
  },
825
825
  });
826
826
 
827
- await expect(workflow.handler(event)).rejects.toBeInstanceOf(ExecutionError);
827
+ await expect(definition.handler(event)).rejects.toBeInstanceOf(ExecutionError);
828
828
  expect(stdoutSpy).toHaveBeenCalledWith(
829
829
  expect.stringContaining(
830
830
  `<__notion_output__>{"_tag":"error","error":{"name":"ExecutionError","message":"Error during app execution: Error: boom"`,
@@ -832,3 +832,113 @@ describe("createWorkflow", () => {
832
832
  );
833
833
  });
834
834
  });
835
+
836
+ describe("connection-bound triggers", () => {
837
+ it("resolves typed trigger callbacks once and preserves event inference", () => {
838
+ let calls = 0;
839
+ const definition = workflow({
840
+ name: "Typed triggers",
841
+ description: "Uses only declared provider keys",
842
+ connections: [connections.slack({ key: "support" }), connections.calendar()],
843
+ triggers: ({ triggers }) => {
844
+ calls += 1;
845
+ expectTypeOf(triggers.slackMessage)
846
+ .parameter(0)
847
+ .toEqualTypeOf<{ connectionKey: "support" } | undefined>();
848
+ expectTypeOf(triggers.calendarEventCreated)
849
+ .parameter(0)
850
+ .toEqualTypeOf<{ connectionKey: "calendar" } | undefined>();
851
+ expectTypeOf(triggers.discordInteraction)
852
+ .parameter(0)
853
+ .toEqualTypeOf<{ connectionKey: never } | undefined>();
854
+ return [
855
+ triggers.slackMessage({ connectionKey: "support" }),
856
+ triggers.calendarEventCreated({ connectionKey: "calendar" }),
857
+ ];
858
+ },
859
+ handler: (event, context) => {
860
+ expectTypeOf(event.type).toEqualTypeOf<
861
+ "slack.message" | "calendar.event.created"
862
+ >();
863
+ expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<
864
+ "slack" | "calendar"
865
+ >();
866
+ },
867
+ });
868
+ expect(calls).toBe(1);
869
+ expect(definition.config.triggers).toEqual([
870
+ { type: "slack.message", connectionKey: "support" },
871
+ { type: "calendar.event.created", connectionKey: "calendar" },
872
+ ]);
873
+ });
874
+
875
+ it("preserves the binding in the manifest and infers the event", () => {
876
+ const definition = workflow({
877
+ name: "Support",
878
+ description: "Watch support messages",
879
+ connections: [connections.slack({ key: "support" })],
880
+ triggers: [triggers.slackMessage({ connectionKey: "support" })],
881
+ handler(event) {
882
+ expectTypeOf(event.type).toEqualTypeOf<"slack.message">();
883
+ },
884
+ });
885
+ expect(definition.config.triggers).toEqual([
886
+ { type: "slack.message", connectionKey: "support" },
887
+ ]);
888
+ });
889
+ it.each([
890
+ { requirements: [] },
891
+ { requirements: [connections.calendar({ key: "support" })] },
892
+ { requirements: [connections.slack({ key: "other" })] },
893
+ ])("rejects missing or incompatible declarations: %j", ({ requirements }) => {
894
+ expect(() =>
895
+ workflow({
896
+ name: "Support",
897
+ description: "",
898
+ connections: requirements,
899
+ triggers: [triggers.slackMessage({ connectionKey: "support" })],
900
+ handler() {},
901
+ }),
902
+ ).toThrow("requires a declared slack connection");
903
+ });
904
+ it("rejects duplicate bindings while allowing distinct connections", () => {
905
+ const configuration = {
906
+ name: "Support",
907
+ description: "",
908
+ connections: [
909
+ connections.slack({ key: "support" }),
910
+ connections.slack({ key: "internal" }),
911
+ ],
912
+ handler() {},
913
+ };
914
+ expect(() =>
915
+ workflow({
916
+ ...configuration,
917
+ triggers: [
918
+ triggers.slackMessage({ connectionKey: "support" }),
919
+ triggers.slackMessage({ connectionKey: "support" }),
920
+ ],
921
+ }),
922
+ ).toThrow("Duplicate");
923
+ expect(() =>
924
+ workflow({
925
+ ...configuration,
926
+ triggers: [
927
+ triggers.slackMessage({ connectionKey: "support" }),
928
+ triggers.slackMessage({ connectionKey: "internal" }),
929
+ ],
930
+ }),
931
+ ).not.toThrow();
932
+ });
933
+ it("preserves existing unkeyed helpers", () => {
934
+ expect(triggers.slackMessage()).toEqual({ type: "slack.message" });
935
+ expect(() =>
936
+ workflow({
937
+ name: "Existing",
938
+ description: "",
939
+ triggers: [triggers.slackMessage()],
940
+ handler() {},
941
+ }),
942
+ ).not.toThrow();
943
+ });
944
+ });
package/src/workflow.ts CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ createWorkflowConnections,
3
+ validateConnectionRequirements,
4
+ validateTriggerConnections,
5
+ type WorkflowConnection,
6
+ type WorkflowConnections,
7
+ } from "./connections.js";
1
8
  import { createHash } from "node:crypto";
2
9
  import { readFile } from "node:fs/promises";
3
10
  import { join } from "node:path";
@@ -8,7 +15,13 @@ import { ExecutionError } from "./error.js";
8
15
  import { writeOutput } from "./output.js";
9
16
  import { resolveRuntimeInput } from "./runtime-input.js";
10
17
  import { readRunMetadata, type RunMetadata } from "./runtime-metadata.js";
11
- import type { WorkflowEventMap, WorkflowTrigger } from "./triggers.generated.js";
18
+ import { createWorkflowTriggers } from "./triggers.generated.js";
19
+ import type {
20
+ WorkflowEventMap,
21
+ WorkflowTrigger,
22
+ WorkflowTriggerCreators,
23
+ WorkflowTriggerForConnections,
24
+ } from "./triggers.generated.js";
12
25
 
13
26
  type HandlerOptions = {
14
27
  /** Return the concrete result instead of writing the output envelope. */
@@ -22,24 +35,14 @@ export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMa
22
35
  export type WorkflowEventForTriggers<T extends readonly WorkflowTrigger[]> =
23
36
  WorkflowEventForTrigger<T[number]>;
24
37
 
25
- /** A connection that a workflow needs before it can run. */
26
- export type WorkflowConnection = {
27
- type: "calendar";
28
- };
29
-
30
- /** Connection requirements supported by workflow capabilities. */
31
- export const connections = {
32
- /** Require an authorized Calendar connection. */
33
- calendar(): WorkflowConnection {
34
- return { type: "calendar" };
35
- },
36
- };
38
+ export { connections, type WorkflowConnection, type WorkflowConnections } from "./connections.js";
37
39
 
38
40
  /**
39
- * Configuration passed to {@link createWorkflow}.
41
+ * Configuration passed to {@link workflow}.
40
42
  */
41
43
  export type WorkflowConfiguration<
42
44
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
45
+ TConnections extends readonly WorkflowConnection[] = readonly WorkflowConnection[],
43
46
  > = {
44
47
  /**
45
48
  * A human-readable name for the workflow, shown in the UI when viewing workflows
@@ -61,16 +64,24 @@ export type WorkflowConfiguration<
61
64
  /**
62
65
  * Connections that must be set up before this workflow can run.
63
66
  */
64
- connections?: readonly WorkflowConnection[];
67
+ connections?: TConnections;
65
68
 
66
69
  handler: (
67
70
  event: WorkflowEventForTriggers<TTriggers>,
68
- context: WorkflowContext,
71
+ context: WorkflowContext<TConnections>,
69
72
  ) => Promise<void> | void;
70
73
  };
71
74
 
75
+ /** Configuration that checks trigger keys against declared connections. */
76
+ export type WorkflowTriggerConfiguration<
77
+ TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
78
+ TConnections extends readonly WorkflowConnection[],
79
+ > = Omit<WorkflowConfiguration<TTriggers, TConnections>, "triggers"> & {
80
+ triggers: (context: { triggers: WorkflowTriggerCreators<NoInfer<TConnections>> }) => TTriggers;
81
+ };
82
+
72
83
  /**
73
- * A workflow capability as returned by {@link createWorkflow}.
84
+ * A workflow capability as returned by {@link workflow}.
74
85
  *
75
86
  * Note that a workflow carries no key of its own — the build tool derives
76
87
  * the capability key from the file the workflow is default-exported from.
@@ -104,9 +115,9 @@ export type Workflow<
104
115
  * ```ts
105
116
  * // src/workflows/onPageCreated.ts
106
117
  * import { triggers } from "@notionhq/apps/triggers";
107
- * import { createWorkflow } from "@notionhq/apps/workflow";
118
+ * import { workflow } from "@notionhq/apps/workflow";
108
119
  *
109
- * export default createWorkflow({
120
+ * export default workflow({
110
121
  * name: "Send Welcome Email",
111
122
  * description: "Sends a welcome email when a new page is added",
112
123
  * triggers: [triggers.notionPageCreated()],
@@ -120,25 +131,38 @@ export type Workflow<
120
131
  * });
121
132
  * ```
122
133
  */
123
- export function createWorkflow<
134
+ export function workflow<
135
+ const TTriggers extends readonly [
136
+ WorkflowTriggerForConnections<NoInfer<TConnections>>,
137
+ ...WorkflowTriggerForConnections<NoInfer<TConnections>>[],
138
+ ],
139
+ const TConnections extends readonly WorkflowConnection[] = readonly [],
140
+ >(configuration: WorkflowTriggerConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
141
+ export function workflow<
124
142
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
125
- >(configuration: WorkflowConfiguration<TTriggers>): Workflow<TTriggers> {
126
- if (configuration.connections !== undefined) {
127
- const connectionTypes = new Set<string>();
128
- for (const connection of configuration.connections) {
129
- if (connectionTypes.has(connection.type)) {
130
- throw new Error(`Duplicate workflow connection: ${connection.type}`);
131
- }
132
- connectionTypes.add(connection.type);
133
- }
134
- }
143
+ const TConnections extends readonly WorkflowConnection[] = readonly [],
144
+ >(configuration: WorkflowConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
145
+ export function workflow<
146
+ const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
147
+ const TConnections extends readonly WorkflowConnection[] = readonly [],
148
+ >(
149
+ configuration:
150
+ | WorkflowConfiguration<TTriggers, TConnections>
151
+ | WorkflowTriggerConfiguration<TTriggers, TConnections>,
152
+ ): Workflow<TTriggers> {
153
+ const triggers =
154
+ typeof configuration.triggers === "function"
155
+ ? configuration.triggers({ triggers: createWorkflowTriggers<TConnections>() })
156
+ : configuration.triggers;
157
+ validateConnectionRequirements(configuration.connections ?? []);
158
+ validateTriggerConnections(triggers, configuration.connections ?? []);
135
159
 
136
160
  return {
137
161
  _tag: "workflow",
138
162
  config: {
139
163
  name: configuration.name,
140
164
  description: configuration.description,
141
- triggers: configuration.triggers,
165
+ triggers,
142
166
  ...(configuration.connections === undefined
143
167
  ? {}
144
168
  : { connections: configuration.connections }),
@@ -150,8 +174,10 @@ export function createWorkflow<
150
174
  try {
151
175
  event = await resolveRuntimeInput(event);
152
176
  const runMetadata = readRunMetadata();
177
+ const baseContext = createCapabilityContext();
153
178
  const capabilityContext: WorkflowContext = {
154
- ...createCapabilityContext(),
179
+ ...baseContext,
180
+ connections: createWorkflowConnections(baseContext.notion),
155
181
  ...runMetadata,
156
182
  step: createStep(runMetadata),
157
183
  };
@@ -213,8 +239,11 @@ type WorkflowStep = {
213
239
  };
214
240
 
215
241
  /** Context passed to a workflow handler. */
216
- export type WorkflowContext = CapabilityContext &
217
- RunMetadata & {
242
+ export type WorkflowContext<
243
+ TConnections extends readonly WorkflowConnection[] = readonly WorkflowConnection[],
244
+ > = CapabilityContext & {
245
+ connections: WorkflowConnections<TConnections>;
246
+ } & RunMetadata & {
218
247
  /**
219
248
  * Run an observable unit of work within the workflow.
220
249
  *