@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
package/src/workflow.test.ts
CHANGED
|
@@ -8,7 +8,13 @@ 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 {
|
|
11
|
+
import {
|
|
12
|
+
connections,
|
|
13
|
+
workflow,
|
|
14
|
+
type StepContext,
|
|
15
|
+
type WorkflowContext,
|
|
16
|
+
type WorkflowWaitDuration,
|
|
17
|
+
} from "./workflow.js";
|
|
12
18
|
|
|
13
19
|
let stdoutSpy: Mock<typeof process.stdout.write>;
|
|
14
20
|
let checkpointDirectory: string | undefined;
|
|
@@ -17,6 +23,7 @@ let metadataDirectory: string | undefined;
|
|
|
17
23
|
beforeEach(async () => {
|
|
18
24
|
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
19
25
|
delete process.env.NOTION_WORKFLOW_STEP_DIRECTORY;
|
|
26
|
+
process.env.NOTION_WORKFLOW_CAPABILITY_KEY = "workflow-key";
|
|
20
27
|
checkpointDirectory = undefined;
|
|
21
28
|
metadataDirectory = await mkdtemp(join(tmpdir(), "notion-workflow-metadata-"));
|
|
22
29
|
await writeRunMetadata();
|
|
@@ -25,8 +32,12 @@ beforeEach(async () => {
|
|
|
25
32
|
afterEach(async () => {
|
|
26
33
|
stdoutSpy.mockRestore();
|
|
27
34
|
delete process.env.NOTION_WORKFLOW_STEP_DIRECTORY;
|
|
35
|
+
delete process.env.NOTION_WORKFLOW_CAPABILITY_KEY;
|
|
28
36
|
delete process.env.NOTION_INPUT_PAYLOAD_PATH;
|
|
29
37
|
delete process.env.NOTION_RUN_METADATA_PATH;
|
|
38
|
+
delete process.env.NOTION_API_BASE_URL;
|
|
39
|
+
delete process.env.NOTION_API_TOKEN;
|
|
40
|
+
vi.unstubAllGlobals();
|
|
30
41
|
if (checkpointDirectory) {
|
|
31
42
|
await rm(checkpointDirectory, { recursive: true, force: true });
|
|
32
43
|
}
|
|
@@ -37,8 +48,8 @@ afterEach(async () => {
|
|
|
37
48
|
|
|
38
49
|
const event = { type: "notion.page.created" } as NotionPageCreatedEvent;
|
|
39
50
|
|
|
40
|
-
async function createWorkflowContext(): Promise<WorkflowContext<
|
|
41
|
-
let context: WorkflowContext<
|
|
51
|
+
async function createWorkflowContext(): Promise<WorkflowContext<Record<never, never>>> {
|
|
52
|
+
let context: WorkflowContext<Record<never, never>> | undefined;
|
|
42
53
|
const definition = workflow({
|
|
43
54
|
name: "Test workflow",
|
|
44
55
|
description: "Captures its workflow context",
|
|
@@ -80,6 +91,167 @@ function emittedStepEvents(): Array<{
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
describe("step", () => {
|
|
94
|
+
it("exposes workflow state only while a step callback is running", async () => {
|
|
95
|
+
const context = await createWorkflowContext();
|
|
96
|
+
let escapedState: StepContext["state"] | undefined;
|
|
97
|
+
|
|
98
|
+
await context.step("Capture state", ({ state }) => {
|
|
99
|
+
escapedState = state;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await expect(escapedState?.get("cursor")).rejects.toThrow(
|
|
103
|
+
"state.get() must be called inside context.step().",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reads and writes durable workflow state inside a step", async () => {
|
|
108
|
+
process.env.NOTION_API_BASE_URL = "https://api.example.com";
|
|
109
|
+
process.env.NOTION_API_TOKEN = "workflow-token";
|
|
110
|
+
const fetchMock = vi
|
|
111
|
+
.fn<typeof fetch>()
|
|
112
|
+
.mockResolvedValueOnce(
|
|
113
|
+
new Response(JSON.stringify({ exists: true, value: "cursor-1" }), {
|
|
114
|
+
status: 200,
|
|
115
|
+
headers: { "content-type": "application/json" },
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
.mockResolvedValueOnce(
|
|
119
|
+
new Response(JSON.stringify({}), {
|
|
120
|
+
status: 200,
|
|
121
|
+
headers: { "content-type": "application/json" },
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
125
|
+
const context = await createWorkflowContext();
|
|
126
|
+
|
|
127
|
+
const cursor = await context.step("Update cursor", async ({ state }) => {
|
|
128
|
+
const current = await state.get("cursor");
|
|
129
|
+
await state.set("cursor", "cursor-2");
|
|
130
|
+
return current;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(cursor).toBe("cursor-1");
|
|
134
|
+
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
135
|
+
1,
|
|
136
|
+
"https://api.example.com/api/v3/workersWorkflowState",
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: expect.objectContaining({
|
|
140
|
+
authorization: "Bearer workflow-token",
|
|
141
|
+
}),
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
operation: "get",
|
|
144
|
+
key: "cursor",
|
|
145
|
+
workerId: "worker-id",
|
|
146
|
+
capabilityKey: "workflow-key",
|
|
147
|
+
}),
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
151
|
+
2,
|
|
152
|
+
"https://api.example.com/api/v3/workersWorkflowState",
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: expect.stringMatching(
|
|
156
|
+
/^\{"operation":"commit","operationId":"run-group-id:[^"]+","mutations":\[\{"operation":"set","key":"cursor","value":"cursor-2"\}\],"workerId":"worker-id","capabilityKey":"workflow-key"\}$/,
|
|
157
|
+
),
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("discards buffered state when the step callback throws", async () => {
|
|
163
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
164
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
165
|
+
const context = await createWorkflowContext();
|
|
166
|
+
|
|
167
|
+
await expect(
|
|
168
|
+
context.step("Do something", ({ state }) => {
|
|
169
|
+
state.set("foo", 42);
|
|
170
|
+
throw new Error("crash");
|
|
171
|
+
}),
|
|
172
|
+
).rejects.toThrow("crash");
|
|
173
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("reads buffered state before the step commits", async () => {
|
|
177
|
+
process.env.NOTION_API_BASE_URL = "https://api.example.com";
|
|
178
|
+
process.env.NOTION_API_TOKEN = "workflow-token";
|
|
179
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
|
|
180
|
+
new Response(JSON.stringify({ success: true }), {
|
|
181
|
+
status: 200,
|
|
182
|
+
headers: { "content-type": "application/json" },
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
186
|
+
const context = await createWorkflowContext();
|
|
187
|
+
|
|
188
|
+
const value = await context.step("Write then read", async ({ state }) => {
|
|
189
|
+
state.set("foo", 42);
|
|
190
|
+
return state.get("foo");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(value).toBe(42);
|
|
194
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("coalesces repeated mutations of a key within the step transaction", async () => {
|
|
198
|
+
process.env.NOTION_API_BASE_URL = "https://api.example.com";
|
|
199
|
+
process.env.NOTION_API_TOKEN = "workflow-token";
|
|
200
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
|
|
201
|
+
new Response(JSON.stringify({ success: true }), {
|
|
202
|
+
status: 200,
|
|
203
|
+
headers: { "content-type": "application/json" },
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
207
|
+
const context = await createWorkflowContext();
|
|
208
|
+
|
|
209
|
+
const value = await context.step("Mutate repeatedly", async ({ state }) => {
|
|
210
|
+
state.set("cursor", "first");
|
|
211
|
+
state.set("cursor", undefined);
|
|
212
|
+
state.set("cursor", "final");
|
|
213
|
+
return state.get("cursor");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(value).toBe("final");
|
|
217
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
218
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
219
|
+
"https://api.example.com/api/v3/workersWorkflowState",
|
|
220
|
+
expect.objectContaining({
|
|
221
|
+
body: expect.stringContaining(
|
|
222
|
+
'"mutations":[{"operation":"set","key":"cursor","value":"final"}]',
|
|
223
|
+
),
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("deletes a key by setting it to undefined", async () => {
|
|
229
|
+
process.env.NOTION_API_BASE_URL = "https://api.example.com";
|
|
230
|
+
process.env.NOTION_API_TOKEN = "workflow-token";
|
|
231
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
|
|
232
|
+
new Response(JSON.stringify({ success: true }), {
|
|
233
|
+
status: 200,
|
|
234
|
+
headers: { "content-type": "application/json" },
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
238
|
+
const context = await createWorkflowContext();
|
|
239
|
+
|
|
240
|
+
const isDeleted = await context.step("Delete state", async ({ state }) => {
|
|
241
|
+
state.set("foo", 42);
|
|
242
|
+
state.set("foo", undefined);
|
|
243
|
+
return (await state.get("foo")) === undefined;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(isDeleted).toBe(true);
|
|
247
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
248
|
+
"https://api.example.com/api/v3/workersWorkflowState",
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
body: expect.stringContaining('"mutations":[{"operation":"delete","key":"foo"}]'),
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
83
255
|
it("provides required run metadata in the workflow context", async () => {
|
|
84
256
|
const context = await createWorkflowContext();
|
|
85
257
|
|
|
@@ -683,6 +855,162 @@ describe("step", () => {
|
|
|
683
855
|
});
|
|
684
856
|
});
|
|
685
857
|
|
|
858
|
+
describe("wait.until", () => {
|
|
859
|
+
it("stops the workflow and returns its absolute resume time", async () => {
|
|
860
|
+
const now = Date.parse("2026-09-17T09:00:00.000Z");
|
|
861
|
+
const resumeAt = new Date(now + 60_000);
|
|
862
|
+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
|
|
863
|
+
let continued = false;
|
|
864
|
+
const definition = workflow({
|
|
865
|
+
name: "Wait for launch",
|
|
866
|
+
description: "Waits until launch time",
|
|
867
|
+
triggers: [triggers.notionPageCreated()],
|
|
868
|
+
handler: async (_event, context) => {
|
|
869
|
+
await context.wait.until("Launch time", { at: resumeAt });
|
|
870
|
+
continued = true;
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
|
|
876
|
+
status: "waiting",
|
|
877
|
+
wait: {
|
|
878
|
+
type: "until",
|
|
879
|
+
name: "Launch time",
|
|
880
|
+
resumeAtMs: resumeAt.getTime(),
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
expect(continued).toBe(false);
|
|
884
|
+
} finally {
|
|
885
|
+
dateNowSpy.mockRestore();
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it("anchors a relative wait in its durable step checkpoint", async () => {
|
|
890
|
+
const directory = await createCheckpointDirectory();
|
|
891
|
+
const initialNow = Date.parse("2026-09-17T09:00:00.000Z");
|
|
892
|
+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(initialNow);
|
|
893
|
+
let continued = false;
|
|
894
|
+
const definition = workflow({
|
|
895
|
+
name: "Wait before following up",
|
|
896
|
+
description: "Waits before continuing",
|
|
897
|
+
triggers: [triggers.notionPageCreated()],
|
|
898
|
+
handler: async (_event, context) => {
|
|
899
|
+
await context.wait.until("Follow-up delay", {
|
|
900
|
+
after: { minutes: 30, seconds: 15 },
|
|
901
|
+
});
|
|
902
|
+
continued = true;
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
const firstResult = await definition.handler(event, { concreteOutput: true });
|
|
908
|
+
expect(firstResult).toMatchObject({
|
|
909
|
+
status: "waiting",
|
|
910
|
+
wait: { resumeAtMs: initialNow + 1_815_000 },
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const successEvent = emittedStepEvents().find(({ event }) => event === "success");
|
|
914
|
+
const stepKey = successEvent?.value.key;
|
|
915
|
+
const resumeAtMs = successEvent?.value.value;
|
|
916
|
+
if (typeof stepKey !== "string" || typeof resumeAtMs !== "number") {
|
|
917
|
+
throw new Error("Wait checkpoint event was not emitted");
|
|
918
|
+
}
|
|
919
|
+
await writeFile(
|
|
920
|
+
checkpointPath(directory, stepKey),
|
|
921
|
+
JSON.stringify({
|
|
922
|
+
version: 0,
|
|
923
|
+
key: stepKey,
|
|
924
|
+
value: resumeAtMs,
|
|
925
|
+
sourceRunId: "first-run",
|
|
926
|
+
sourceStepId: "wait-step",
|
|
927
|
+
}),
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
dateNowSpy.mockReturnValue(initialNow + 7_200_000);
|
|
931
|
+
await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
|
|
932
|
+
status: "success",
|
|
933
|
+
});
|
|
934
|
+
expect(continued).toBe(true);
|
|
935
|
+
} finally {
|
|
936
|
+
dateNowSpy.mockRestore();
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it("uses composite keys to distinguish waits in a loop", async () => {
|
|
941
|
+
const now = Date.parse("2026-09-17T09:00:00.000Z");
|
|
942
|
+
const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(now);
|
|
943
|
+
const itemIds = ["first", "second"];
|
|
944
|
+
const definition = workflow({
|
|
945
|
+
name: "Wait between items",
|
|
946
|
+
description: "Uses one stable display name for repeated waits",
|
|
947
|
+
triggers: [triggers.notionPageCreated()],
|
|
948
|
+
handler: async (_event, context) => {
|
|
949
|
+
for (const itemId of itemIds) {
|
|
950
|
+
await context.wait.until("Item delay", {
|
|
951
|
+
key: ["item-delay", itemId],
|
|
952
|
+
at: new Date(now),
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
|
|
960
|
+
status: "success",
|
|
961
|
+
});
|
|
962
|
+
expect(
|
|
963
|
+
emittedStepEvents()
|
|
964
|
+
.filter(({ event }) => event === "success")
|
|
965
|
+
.map(({ value }) => value.key),
|
|
966
|
+
).toEqual([
|
|
967
|
+
workflowStepKey(["wait", "until", "item-delay", "first"]),
|
|
968
|
+
workflowStepKey(["wait", "until", "item-delay", "second"]),
|
|
969
|
+
]);
|
|
970
|
+
} finally {
|
|
971
|
+
dateNowSpy.mockRestore();
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it("rejects waits that can outlive their durable checkpoint", async () => {
|
|
976
|
+
const definition = workflow({
|
|
977
|
+
name: "Wait too long",
|
|
978
|
+
description: "Rejects an unsupported wait",
|
|
979
|
+
triggers: [triggers.notionPageCreated()],
|
|
980
|
+
handler: async (_event, context) => {
|
|
981
|
+
await context.wait.until("Beyond retention", {
|
|
982
|
+
after: { days: 7 },
|
|
983
|
+
});
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
|
|
988
|
+
"Workflow waits must be shorter than 7 days.",
|
|
989
|
+
);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it.each([
|
|
993
|
+
[{ hours: 0 }, "positive finite integers"],
|
|
994
|
+
[{ minutes: 1.5 }, "positive finite integers"],
|
|
995
|
+
[{}, "at least one unit"],
|
|
996
|
+
])("rejects invalid relative durations", async (after, expectedMessage) => {
|
|
997
|
+
const definition = workflow({
|
|
998
|
+
name: "Wait for a valid duration",
|
|
999
|
+
description: "Rejects an invalid duration",
|
|
1000
|
+
triggers: [triggers.notionPageCreated()],
|
|
1001
|
+
handler: async (_event, context) => {
|
|
1002
|
+
await context.wait.until("Invalid duration", {
|
|
1003
|
+
after: after as WorkflowWaitDuration,
|
|
1004
|
+
});
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
|
|
1009
|
+
expectedMessage,
|
|
1010
|
+
);
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
686
1014
|
async function createCheckpointDirectory(): Promise<string> {
|
|
687
1015
|
checkpointDirectory = await mkdtemp(join(tmpdir(), "apps-step-checkpoints-"));
|
|
688
1016
|
process.env.NOTION_WORKFLOW_STEP_DIRECTORY = checkpointDirectory;
|
|
@@ -743,23 +1071,54 @@ describe("workflow", () => {
|
|
|
743
1071
|
name: "Schedule Follow-up",
|
|
744
1072
|
description: "Schedules a follow-up after a page is created",
|
|
745
1073
|
triggers: [triggers.notionPageCreated()],
|
|
746
|
-
connections:
|
|
1074
|
+
connections: { calendar: connections.calendar() },
|
|
747
1075
|
handler: () => {},
|
|
748
1076
|
});
|
|
749
1077
|
|
|
750
|
-
expect(definition.config.connections).toEqual([{ type: "calendar" }]);
|
|
1078
|
+
expect(definition.config.connections).toEqual([{ type: "calendar", key: "calendar" }]);
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
it("keeps declaration keys stable after the input object changes", async () => {
|
|
1082
|
+
const declarations = { support: connections.slack() };
|
|
1083
|
+
const seen: string[][] = [];
|
|
1084
|
+
const definition = workflow({
|
|
1085
|
+
name: "Snapshot connections",
|
|
1086
|
+
description: "Captures declared keys",
|
|
1087
|
+
connections: declarations,
|
|
1088
|
+
triggers: [triggers.notionPageCreated()],
|
|
1089
|
+
handler(_event, context) {
|
|
1090
|
+
seen.push(Object.keys(context.connections));
|
|
1091
|
+
},
|
|
1092
|
+
});
|
|
1093
|
+
Object.assign(declarations, { support: connections.calendar(), team: connections.slack() });
|
|
1094
|
+
expect(definition.config.connections).toEqual([{ type: "slack", key: "support" }]);
|
|
1095
|
+
await definition.handler(event, { concreteOutput: true });
|
|
1096
|
+
expect(seen).toEqual([["support"]]);
|
|
1097
|
+
});
|
|
1098
|
+
it("rejects inherited declaration objects before cloning", () => {
|
|
1099
|
+
const declarations = { support: connections.slack() };
|
|
1100
|
+
Object.setPrototypeOf(declarations, { extra: connections.calendar() });
|
|
1101
|
+
expect(() =>
|
|
1102
|
+
workflow({
|
|
1103
|
+
name: "Inherited keys",
|
|
1104
|
+
description: "Reject prototype declarations",
|
|
1105
|
+
connections: declarations,
|
|
1106
|
+
triggers: [triggers.notionPageCreated()],
|
|
1107
|
+
handler() {},
|
|
1108
|
+
}),
|
|
1109
|
+
).toThrow("only own");
|
|
751
1110
|
});
|
|
752
1111
|
|
|
753
|
-
it("rejects
|
|
1112
|
+
it("rejects invalid connection property names", () => {
|
|
754
1113
|
expect(() =>
|
|
755
1114
|
workflow({
|
|
756
1115
|
name: "Schedule Follow-up",
|
|
757
1116
|
description: "Schedules a follow-up after a page is created",
|
|
758
1117
|
triggers: [triggers.notionPageCreated()],
|
|
759
|
-
connections:
|
|
1118
|
+
connections: { "bad/key": connections.calendar() },
|
|
760
1119
|
handler: () => {},
|
|
761
1120
|
}),
|
|
762
|
-
).toThrowError("
|
|
1121
|
+
).toThrowError("Invalid workflow connection key: bad/key");
|
|
763
1122
|
});
|
|
764
1123
|
|
|
765
1124
|
it("writes a success envelope after the handler runs", async () => {
|
|
@@ -839,7 +1198,7 @@ describe("connection-bound triggers", () => {
|
|
|
839
1198
|
const definition = workflow({
|
|
840
1199
|
name: "Typed triggers",
|
|
841
1200
|
description: "Uses only declared provider keys",
|
|
842
|
-
connections:
|
|
1201
|
+
connections: { support: connections.slack(), calendar: connections.calendar() },
|
|
843
1202
|
triggers: ({ triggers }) => {
|
|
844
1203
|
calls += 1;
|
|
845
1204
|
expectTypeOf(triggers.slackMessage)
|
|
@@ -861,7 +1220,7 @@ describe("connection-bound triggers", () => {
|
|
|
861
1220
|
"slack.message" | "calendar.event.created"
|
|
862
1221
|
>();
|
|
863
1222
|
expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<
|
|
864
|
-
"
|
|
1223
|
+
"support" | "calendar"
|
|
865
1224
|
>();
|
|
866
1225
|
},
|
|
867
1226
|
});
|
|
@@ -876,7 +1235,7 @@ describe("connection-bound triggers", () => {
|
|
|
876
1235
|
const definition = workflow({
|
|
877
1236
|
name: "Support",
|
|
878
1237
|
description: "Watch support messages",
|
|
879
|
-
connections:
|
|
1238
|
+
connections: { support: connections.slack() },
|
|
880
1239
|
triggers: [triggers.slackMessage({ connectionKey: "support" })],
|
|
881
1240
|
handler(event) {
|
|
882
1241
|
expectTypeOf(event.type).toEqualTypeOf<"slack.message">();
|
|
@@ -887,9 +1246,9 @@ describe("connection-bound triggers", () => {
|
|
|
887
1246
|
]);
|
|
888
1247
|
});
|
|
889
1248
|
it.each([
|
|
890
|
-
{ requirements:
|
|
891
|
-
{ requirements:
|
|
892
|
-
{ requirements:
|
|
1249
|
+
{ requirements: {} },
|
|
1250
|
+
{ requirements: { support: connections.calendar() } },
|
|
1251
|
+
{ requirements: { other: connections.slack() } },
|
|
893
1252
|
])("rejects missing or incompatible declarations: %j", ({ requirements }) => {
|
|
894
1253
|
expect(() =>
|
|
895
1254
|
workflow({
|
|
@@ -905,10 +1264,7 @@ describe("connection-bound triggers", () => {
|
|
|
905
1264
|
const configuration = {
|
|
906
1265
|
name: "Support",
|
|
907
1266
|
description: "",
|
|
908
|
-
connections:
|
|
909
|
-
connections.slack({ key: "support" }),
|
|
910
|
-
connections.slack({ key: "internal" }),
|
|
911
|
-
],
|
|
1267
|
+
connections: { support: connections.slack(), internal: connections.slack() },
|
|
912
1268
|
handler() {},
|
|
913
1269
|
};
|
|
914
1270
|
expect(() =>
|