@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.
- package/README.md +27 -2
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/notion-as-code/database.d.ts +3 -2
- package/dist/notion-as-code/database.d.ts.map +1 -1
- 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 +3 -0
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +2 -1
- package/dist/notion-as-code/intents.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 +469 -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/triggers.generated.d.ts +3 -3
- package/dist/triggers.generated.d.ts.map +1 -1
- package/dist/workflow.d.ts +8 -8
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +12 -6
- 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 +8 -2
- package/src/connections.test.ts +205 -41
- package/src/connections.ts +144 -38
- package/src/index.ts +1 -0
- package/src/notion-as-code/database.ts +3 -3
- package/src/notion-as-code/handles.ts +2 -0
- package/src/notion-as-code/index.ts +55 -0
- package/src/notion-as-code/intents.ts +2 -2
- 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 +554 -0
- package/src/oauth.ts +40 -0
- package/src/providers.generated.ts +68 -163
- package/src/triggers.generated.ts +4 -4
- package/src/workflow-connections-types.test.ts +16 -10
- package/src/workflow-types.test.ts +88 -16
- package/src/workflow.test.ts +45 -17
- package/src/workflow.ts +31 -17
package/src/connections.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Client } from "@notionhq/client";
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
connections,
|
|
5
5
|
createWorkflowConnections,
|
|
6
|
-
|
|
6
|
+
normalizeConnectionRequirements,
|
|
7
|
+
type WorkflowConnections,
|
|
7
8
|
} from "./connections.js";
|
|
8
9
|
|
|
9
10
|
afterEach(() => {
|
|
@@ -12,37 +13,95 @@ afterEach(() => {
|
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
describe("connection requirements", () => {
|
|
15
|
-
it("
|
|
16
|
-
expect(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
it("normalizes object keys into manifest requirements", () => {
|
|
17
|
+
expect(
|
|
18
|
+
normalizeConnectionRequirements({
|
|
19
|
+
work: connections.calendar(),
|
|
20
|
+
support: connections.slack(),
|
|
21
|
+
team: connections.slack(),
|
|
22
|
+
}),
|
|
23
|
+
).toEqual([
|
|
24
|
+
{ type: "calendar", key: "work" },
|
|
25
|
+
{ type: "slack", key: "support" },
|
|
26
|
+
{ type: "slack", key: "team" },
|
|
27
|
+
]);
|
|
23
28
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
it.each(["bad/key", "__proto__", "constructor", "prototype"])(
|
|
30
|
+
"rejects invalid or prototype-sensitive keys: %s",
|
|
31
|
+
(key) => {
|
|
32
|
+
expect(() => normalizeConnectionRequirements({ [key]: connections.slack() })).toThrow(
|
|
33
|
+
"Invalid",
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
it("rejects excessive requirements", () => {
|
|
38
|
+
const requirements: Record<string, { type: string }> = {};
|
|
39
|
+
for (let i = 0; i < 101; i++) requirements[`team${i}`] = connections.slack();
|
|
40
|
+
expect(() => normalizeConnectionRequirements(requirements)).toThrow("100");
|
|
32
41
|
});
|
|
33
|
-
it("rejects
|
|
34
|
-
expect(() =>
|
|
35
|
-
validateConnectionRequirements([connections.slack({ key: "bad/key" })]),
|
|
36
|
-
).toThrow("Invalid");
|
|
42
|
+
it("rejects old arrays and declarations with hidden or inherited keys", () => {
|
|
37
43
|
expect(() =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
Reflect.apply(normalizeConnectionRequirements, undefined, [[connections.slack()]]),
|
|
45
|
+
).toThrow("must be an object");
|
|
46
|
+
const inherited = { support: connections.slack() };
|
|
47
|
+
Object.setPrototypeOf(inherited, { hidden: connections.calendar() });
|
|
48
|
+
expect(() => normalizeConnectionRequirements(inherited)).toThrow("only own");
|
|
49
|
+
const hidden = { support: connections.slack() };
|
|
50
|
+
Object.defineProperty(hidden, "hidden", { value: connections.calendar() });
|
|
51
|
+
expect(() => normalizeConnectionRequirements(hidden)).toThrow("enumerable strings");
|
|
52
|
+
const symbolic = { [Symbol("support")]: connections.slack() };
|
|
53
|
+
expect(() => normalizeConnectionRequirements(symbolic)).toThrow("enumerable strings");
|
|
54
|
+
const legacyDeclaration = { support: { ...connections.slack(), key: "other" } };
|
|
55
|
+
expect(() => normalizeConnectionRequirements(legacyDeclaration)).toThrow("object property");
|
|
42
56
|
});
|
|
43
57
|
});
|
|
44
58
|
|
|
45
59
|
describe("workflow connection runtime", () => {
|
|
60
|
+
it("invokes separate bindings for multiple keys of the same provider", async () => {
|
|
61
|
+
vi.stubEnv(
|
|
62
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
63
|
+
JSON.stringify([
|
|
64
|
+
{ key: "support", type: "slack", id: "support-id" },
|
|
65
|
+
{ key: "team", type: "slack", id: "team-id" },
|
|
66
|
+
{ key: "undeclared", type: "slack", id: "hidden-id" },
|
|
67
|
+
]),
|
|
68
|
+
);
|
|
69
|
+
const notion = new Client({ auth: "test" });
|
|
70
|
+
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: undefined });
|
|
71
|
+
const declarations = { support: connections.slack(), team: connections.slack() };
|
|
72
|
+
const runtime = createWorkflowConnections(notion, declarations);
|
|
73
|
+
Object.assign(declarations, {
|
|
74
|
+
support: connections.calendar(),
|
|
75
|
+
injected: connections.slack(),
|
|
76
|
+
});
|
|
77
|
+
await runtime.support.findUserByEmail({ email: "support@example.com" });
|
|
78
|
+
await runtime.team.findUserByEmail({ email: "team@example.com" });
|
|
79
|
+
expect(request.mock.calls.map(([input]) => input.body)).toEqual([
|
|
80
|
+
{
|
|
81
|
+
connection: "support-id",
|
|
82
|
+
action: "findUserByEmail",
|
|
83
|
+
args: { email: "support@example.com" },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
connection: "team-id",
|
|
87
|
+
action: "findUserByEmail",
|
|
88
|
+
args: { email: "team@example.com" },
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
expect(Object.keys(runtime)).toEqual(["support", "team"]);
|
|
92
|
+
expect(Reflect.get(runtime, "undeclared")).toBeUndefined();
|
|
93
|
+
expect(Reflect.get(runtime, "injected")).toBeUndefined();
|
|
94
|
+
expect(Reflect.get(runtime, "toString")).toBeUndefined();
|
|
95
|
+
expect(runtime.support).toBe(runtime.support);
|
|
96
|
+
});
|
|
97
|
+
it("does not parse missing or malformed bindings until a declared client is used", () => {
|
|
98
|
+
vi.stubEnv("NOTION_CONNECTION_BINDINGS", "not json");
|
|
99
|
+
const runtime = createWorkflowConnections(new Client(), { support: connections.slack() });
|
|
100
|
+
expect(Object.keys(runtime)).toEqual(["support"]);
|
|
101
|
+
expect(() => runtime.support).toThrow();
|
|
102
|
+
expect(Object.keys(createWorkflowConnections(new Client(), {}))).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
46
105
|
it("preserves pagination through a generated OAuth provider method", async () => {
|
|
47
106
|
vi.stubEnv(
|
|
48
107
|
"NOTION_CONNECTION_BINDINGS",
|
|
@@ -52,9 +111,9 @@ describe("workflow connection runtime", () => {
|
|
|
52
111
|
const request = vi
|
|
53
112
|
.spyOn(notion, "request")
|
|
54
113
|
.mockResolvedValue({ result: { files: [], nextPageToken: "next" } });
|
|
55
|
-
const result = await createWorkflowConnections(notion
|
|
56
|
-
.googleDriveOauth(
|
|
57
|
-
|
|
114
|
+
const result = await createWorkflowConnections(notion, {
|
|
115
|
+
documents: connections.googleDriveOauth(),
|
|
116
|
+
}).documents.listFiles({ folderId: "folder", pageToken: "previous" });
|
|
58
117
|
expect(result.nextPageToken).toBe("next");
|
|
59
118
|
expect(request).toHaveBeenCalledWith({
|
|
60
119
|
path: "tools/run/internal",
|
|
@@ -73,9 +132,9 @@ describe("workflow connection runtime", () => {
|
|
|
73
132
|
);
|
|
74
133
|
const notion = new Client({ auth: "test" });
|
|
75
134
|
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: [] });
|
|
76
|
-
await createWorkflowConnections(notion)
|
|
77
|
-
|
|
78
|
-
|
|
135
|
+
await createWorkflowConnections(notion, { documents: connections.box() }).documents.search({
|
|
136
|
+
question: "Quarterly report",
|
|
137
|
+
});
|
|
79
138
|
expect(request).toHaveBeenCalledWith({
|
|
80
139
|
path: "tools/run/internal",
|
|
81
140
|
method: "post",
|
|
@@ -91,7 +150,9 @@ describe("workflow connection runtime", () => {
|
|
|
91
150
|
vi.stubEnv("NOTION_CONNECTION_BINDINGS", '[{"key":"support","type":"calendar","id":"a"}]');
|
|
92
151
|
const notion = new Client({ auth: "test" });
|
|
93
152
|
const request = vi.spyOn(notion, "request");
|
|
94
|
-
expect(
|
|
153
|
+
expect(
|
|
154
|
+
() => createWorkflowConnections(notion, { support: connections.slack() }).support,
|
|
155
|
+
).toThrow("not slack");
|
|
95
156
|
expect(request).not.toHaveBeenCalled();
|
|
96
157
|
});
|
|
97
158
|
it("invokes a typed Calendar method with the default binding", async () => {
|
|
@@ -101,7 +162,9 @@ describe("workflow connection runtime", () => {
|
|
|
101
162
|
);
|
|
102
163
|
const notion = new Client({ auth: "test" });
|
|
103
164
|
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: { accounts: [] } });
|
|
104
|
-
const result = await createWorkflowConnections(notion
|
|
165
|
+
const result = await createWorkflowConnections(notion, {
|
|
166
|
+
calendar: connections.calendar(),
|
|
167
|
+
}).calendar.listCalendars({});
|
|
105
168
|
expect(result.accounts).toEqual([]);
|
|
106
169
|
expect(request).toHaveBeenCalledWith({
|
|
107
170
|
path: "tools/run/internal",
|
|
@@ -117,9 +180,12 @@ describe("workflow connection runtime", () => {
|
|
|
117
180
|
);
|
|
118
181
|
const notion = new Client({ auth: "test" });
|
|
119
182
|
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: { ok: true } });
|
|
120
|
-
const runtime = createWorkflowConnections(notion
|
|
183
|
+
const runtime = createWorkflowConnections(notion, {
|
|
184
|
+
support: connections.slack(),
|
|
185
|
+
team: connections.slack(),
|
|
186
|
+
});
|
|
121
187
|
await expect(
|
|
122
|
-
runtime.
|
|
188
|
+
runtime.support.findUserByEmail({ email: "person@example.com" }),
|
|
123
189
|
).resolves.toEqual({
|
|
124
190
|
ok: true,
|
|
125
191
|
});
|
|
@@ -132,7 +198,7 @@ describe("workflow connection runtime", () => {
|
|
|
132
198
|
args: { email: "person@example.com" },
|
|
133
199
|
},
|
|
134
200
|
});
|
|
135
|
-
expect(() => runtime.
|
|
201
|
+
expect(() => runtime.team).toThrow("not bound");
|
|
136
202
|
expect(request).toHaveBeenCalledTimes(1);
|
|
137
203
|
});
|
|
138
204
|
it.each([
|
|
@@ -141,17 +207,115 @@ describe("workflow connection runtime", () => {
|
|
|
141
207
|
'[{"key":"support","type":"slack","id":"a"},{"key":"support","type":"slack","id":"b"}]',
|
|
142
208
|
])("rejects malformed or ambiguous bindings: %s", (metadata) => {
|
|
143
209
|
vi.stubEnv("NOTION_CONNECTION_BINDINGS", metadata);
|
|
144
|
-
expect(
|
|
210
|
+
expect(
|
|
211
|
+
() => createWorkflowConnections(new Client(), { support: connections.slack() }).support,
|
|
212
|
+
).toThrow();
|
|
145
213
|
});
|
|
146
214
|
it("propagates permission failures without retrying an alternate connection", async () => {
|
|
147
215
|
vi.stubEnv("NOTION_CONNECTION_BINDINGS", '[{"key":"support","type":"slack","id":"a"}]');
|
|
148
216
|
const notion = new Client({ auth: "test" });
|
|
149
217
|
const request = vi.spyOn(notion, "request").mockRejectedValue(new Error("forbidden"));
|
|
150
218
|
await expect(
|
|
151
|
-
createWorkflowConnections(notion
|
|
152
|
-
.slack(
|
|
153
|
-
|
|
219
|
+
createWorkflowConnections(notion, {
|
|
220
|
+
support: connections.slack(),
|
|
221
|
+
}).support.findUserByEmail({ email: "person@example.com" }),
|
|
154
222
|
).rejects.toThrow("forbidden");
|
|
155
223
|
expect(request).toHaveBeenCalledTimes(1);
|
|
156
224
|
});
|
|
157
225
|
});
|
|
226
|
+
|
|
227
|
+
describe("generic OAuth connections", () => {
|
|
228
|
+
const config = {
|
|
229
|
+
authorizationEndpoint: "https://github.com/login/oauth/authorize",
|
|
230
|
+
tokenEndpoint: "https://github.com/login/oauth/access_token",
|
|
231
|
+
clientId: "client",
|
|
232
|
+
clientSecretEnv: "GITHUB_CLIENT_SECRET",
|
|
233
|
+
scope: "repo",
|
|
234
|
+
};
|
|
235
|
+
it("declares configuration with a secret reference", () => {
|
|
236
|
+
expect(connections.oauth(config)).toEqual({ type: "oauth", oauth: config });
|
|
237
|
+
const declaration = connections.oauth(config);
|
|
238
|
+
expectTypeOf(declaration.type).toEqualTypeOf<"oauth">();
|
|
239
|
+
expectTypeOf<
|
|
240
|
+
keyof WorkflowConnections<{ github: typeof declaration }>
|
|
241
|
+
>().toEqualTypeOf<"github">();
|
|
242
|
+
expectTypeOf<keyof WorkflowConnections<Record<never, never>>>().toEqualTypeOf<never>();
|
|
243
|
+
expect(normalizeConnectionRequirements({ github: declaration })).toEqual([
|
|
244
|
+
{ type: "oauth", key: "github", oauth: config },
|
|
245
|
+
]);
|
|
246
|
+
});
|
|
247
|
+
it("returns only the token bound to this workflow execution", async () => {
|
|
248
|
+
vi.stubEnv(
|
|
249
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
250
|
+
JSON.stringify([{ key: "github", type: "oauth", id: "ab-cd" }]),
|
|
251
|
+
);
|
|
252
|
+
vi.stubEnv("NOTION_OAUTH_CONNECTION_ABCD", "scoped-token");
|
|
253
|
+
const notion = new Client({ auth: "test" });
|
|
254
|
+
const request = vi.spyOn(notion, "request");
|
|
255
|
+
expect(
|
|
256
|
+
await createWorkflowConnections(notion, {
|
|
257
|
+
github: connections.oauth(config),
|
|
258
|
+
}).github.accessToken(),
|
|
259
|
+
).toBe("scoped-token");
|
|
260
|
+
expect(request).not.toHaveBeenCalled();
|
|
261
|
+
expect(
|
|
262
|
+
() => createWorkflowConnections(notion, { other: connections.oauth(config) }).other,
|
|
263
|
+
).toThrow("not bound");
|
|
264
|
+
});
|
|
265
|
+
it("keeps separately keyed OAuth tokens isolated and omits undeclared bindings", async () => {
|
|
266
|
+
vi.stubEnv(
|
|
267
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
268
|
+
JSON.stringify([
|
|
269
|
+
{ key: "github", type: "oauth", id: "first-id" },
|
|
270
|
+
{ key: "other", type: "oauth", id: "second-id" },
|
|
271
|
+
{ key: "undeclared", type: "oauth", id: "hidden-id" },
|
|
272
|
+
]),
|
|
273
|
+
);
|
|
274
|
+
vi.stubEnv("NOTION_OAUTH_CONNECTION_FIRSTID", "first-token");
|
|
275
|
+
vi.stubEnv("NOTION_OAUTH_CONNECTION_SECONDID", "second-token");
|
|
276
|
+
const runtime = createWorkflowConnections(new Client({ auth: "test" }), {
|
|
277
|
+
github: connections.oauth(config),
|
|
278
|
+
other: connections.oauth(config),
|
|
279
|
+
});
|
|
280
|
+
expect(await runtime.github.accessToken()).toBe("first-token");
|
|
281
|
+
expect(await runtime.other.accessToken()).toBe("second-token");
|
|
282
|
+
expect(Object.keys(runtime)).toEqual(["github", "other"]);
|
|
283
|
+
expect(Reflect.get(runtime, "undeclared")).toBeUndefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("requires explicit authorization and does not fall back to worker OAuth", async () => {
|
|
287
|
+
vi.stubEnv(
|
|
288
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
289
|
+
JSON.stringify([{ key: "github", type: "oauth", id: "unconfigured" }]),
|
|
290
|
+
);
|
|
291
|
+
vi.stubEnv("OAUTH_676974687562_ACCESS_TOKEN", "global-token");
|
|
292
|
+
await expect(
|
|
293
|
+
createWorkflowConnections(new Client({ auth: "test" }), {
|
|
294
|
+
github: connections.oauth(config),
|
|
295
|
+
}).github.accessToken(),
|
|
296
|
+
).rejects.toThrow("requires authorization");
|
|
297
|
+
});
|
|
298
|
+
it("rejects a provider connection used as generic OAuth", () => {
|
|
299
|
+
vi.stubEnv(
|
|
300
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
301
|
+
JSON.stringify([{ key: "github", type: "slack", id: "connection" }]),
|
|
302
|
+
);
|
|
303
|
+
expect(
|
|
304
|
+
() =>
|
|
305
|
+
createWorkflowConnections(new Client({ auth: "test" }), {
|
|
306
|
+
github: connections.oauth(config),
|
|
307
|
+
}).github,
|
|
308
|
+
).toThrow("not oauth");
|
|
309
|
+
});
|
|
310
|
+
it.each(["http://localhost/token", "https://user:password@example.com/token"])(
|
|
311
|
+
"rejects unsafe token endpoint %s",
|
|
312
|
+
(tokenEndpoint) => {
|
|
313
|
+
expect(() => connections.oauth({ ...config, tokenEndpoint })).toThrow("HTTPS URLs");
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
it("protects the callback state and PKCE parameters", () => {
|
|
317
|
+
expect(() =>
|
|
318
|
+
connections.oauth({ ...config, authorizationParams: { state: "forged" } }),
|
|
319
|
+
).toThrow("protocol fields");
|
|
320
|
+
});
|
|
321
|
+
});
|
package/src/connections.ts
CHANGED
|
@@ -1,61 +1,127 @@
|
|
|
1
|
+
import { validateOAuthConfiguration, type OAuthConfiguration } from "./oauth.js";
|
|
1
2
|
import { connectionTriggerDefinitions } from "./connection-trigger-definitions.generated.js";
|
|
2
3
|
import type { WorkflowTrigger } from "./triggers.generated.js";
|
|
3
4
|
import type { Client } from "@notionhq/client";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
connections as providerConnections,
|
|
7
|
+
createConnectionClient,
|
|
8
|
+
type ConnectionClients,
|
|
9
|
+
} from "./providers.generated.js";
|
|
5
10
|
import type { ConnectionActionInvoker } from "./connection-actions.js";
|
|
6
11
|
|
|
7
|
-
/** A setup requirement. The
|
|
8
|
-
export type WorkflowConnection<TProvider extends string = string
|
|
12
|
+
/** A setup requirement. The property name in a workflow supplies its key. */
|
|
13
|
+
export type WorkflowConnection<TProvider extends string = string> = {
|
|
9
14
|
type: TProvider;
|
|
10
|
-
|
|
15
|
+
oauth?: OAuthConfiguration;
|
|
11
16
|
};
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
export type WorkflowConnectionDeclarations = Readonly<Record<string, WorkflowConnection>>;
|
|
19
|
+
|
|
20
|
+
/** Serialized connection requirement sent in the workflow manifest. */
|
|
21
|
+
export type WorkflowConnectionRequirement = WorkflowConnection & { key: string };
|
|
22
|
+
|
|
23
|
+
/** Declared property names belonging to one provider. */
|
|
14
24
|
export type WorkflowConnectionKeys<
|
|
15
|
-
TConnections extends
|
|
25
|
+
TConnections extends WorkflowConnectionDeclarations,
|
|
16
26
|
TProvider extends string,
|
|
17
|
-
> =
|
|
18
|
-
|
|
19
|
-
type
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: never;
|
|
27
|
+
> = Extract<
|
|
28
|
+
{
|
|
29
|
+
[TKey in keyof TConnections]: TConnections[TKey]["type"] extends TProvider ? TKey : never;
|
|
30
|
+
}[keyof TConnections],
|
|
31
|
+
string
|
|
32
|
+
>;
|
|
33
|
+
|
|
25
34
|
type ConnectionBinding = { key: string; type: string; id: string };
|
|
26
35
|
|
|
27
36
|
/** Requirements are declarations, not credentials or permission grants. */
|
|
28
|
-
export
|
|
37
|
+
export const connections = {
|
|
38
|
+
...providerConnections,
|
|
39
|
+
oauth(options: OAuthConfiguration): WorkflowConnection<"oauth"> {
|
|
40
|
+
validateOAuthConfiguration(options);
|
|
41
|
+
return { type: "oauth", oauth: structuredClone(options) };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export type { OAuthConfiguration } from "./oauth.js";
|
|
45
|
+
export type OAuthConnection = { accessToken(): Promise<string> };
|
|
46
|
+
|
|
47
|
+
type WorkflowConnectionClients = ConnectionClients & { oauth: OAuthConnection };
|
|
48
|
+
|
|
49
|
+
type WorkflowConnectionClient = WorkflowConnectionClients[keyof WorkflowConnectionClients];
|
|
29
50
|
|
|
30
51
|
export type WorkflowConnections<
|
|
31
|
-
TConnections extends
|
|
32
|
-
> =
|
|
52
|
+
TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
|
|
53
|
+
> = {
|
|
54
|
+
readonly [TKey in keyof TConnections]: WorkflowConnectionClients[Extract<
|
|
55
|
+
keyof WorkflowConnectionClients,
|
|
56
|
+
TConnections[TKey]["type"]
|
|
57
|
+
>];
|
|
58
|
+
};
|
|
33
59
|
|
|
34
60
|
/** @internal */
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
export function normalizeConnectionRequirements(
|
|
62
|
+
declarations: WorkflowConnectionDeclarations,
|
|
63
|
+
): WorkflowConnectionRequirement[] {
|
|
64
|
+
if (typeof declarations !== "object" || declarations === null || Array.isArray(declarations)) {
|
|
65
|
+
throw new Error("Workflow connections must be an object keyed by connection name.");
|
|
66
|
+
}
|
|
67
|
+
const prototype: unknown = Object.getPrototypeOf(declarations);
|
|
68
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
69
|
+
throw new Error("Workflow connections must contain only own connection declarations.");
|
|
70
|
+
}
|
|
71
|
+
const keys = Object.keys(declarations);
|
|
72
|
+
if (Reflect.ownKeys(declarations).length !== keys.length) {
|
|
73
|
+
throw new Error("Workflow connection keys must be enumerable strings.");
|
|
74
|
+
}
|
|
75
|
+
if (keys.length > 100) throw new Error("Workflows support at most 100 connections.");
|
|
76
|
+
return Object.entries(declarations).map(([key, requirement]) => {
|
|
77
|
+
if (
|
|
78
|
+
!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(key) ||
|
|
79
|
+
["__proto__", "prototype", "constructor"].includes(key)
|
|
80
|
+
) {
|
|
42
81
|
throw new Error("Invalid workflow connection key: " + key);
|
|
43
82
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
83
|
+
if (
|
|
84
|
+
typeof requirement !== "object" ||
|
|
85
|
+
requirement === null ||
|
|
86
|
+
typeof requirement.type !== "string" ||
|
|
87
|
+
!requirement.type ||
|
|
88
|
+
!Object.hasOwn(requirement, "type")
|
|
89
|
+
) {
|
|
90
|
+
throw new Error("A workflow connection needs a provider type.");
|
|
91
|
+
}
|
|
92
|
+
if ("key" in requirement)
|
|
93
|
+
throw new Error(
|
|
94
|
+
"Declare a connection key as its workflow object property, not in the provider options.",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (requirement.type === "oauth") {
|
|
98
|
+
if (!requirement.oauth)
|
|
99
|
+
throw new Error("OAuth connections require provider configuration.");
|
|
100
|
+
validateOAuthConfiguration(requirement.oauth);
|
|
101
|
+
} else if (requirement.oauth !== undefined) {
|
|
102
|
+
throw new Error("OAuth configuration is only valid on OAuth connections.");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type: requirement.type,
|
|
107
|
+
key,
|
|
108
|
+
...(requirement.oauth === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { oauth: structuredClone(requirement.oauth) }),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
47
113
|
}
|
|
48
114
|
|
|
49
115
|
/** @internal */
|
|
50
116
|
export function validateTriggerConnections(
|
|
51
117
|
triggers: readonly WorkflowTrigger[],
|
|
52
|
-
requirements: readonly
|
|
118
|
+
requirements: readonly WorkflowConnectionRequirement[],
|
|
53
119
|
): void {
|
|
54
120
|
const seen = new Set<string>();
|
|
55
121
|
for (const trigger of triggers) {
|
|
56
122
|
if (!("connectionKey" in trigger) || trigger.connectionKey === undefined) continue;
|
|
57
123
|
const requirement = requirements.find(
|
|
58
|
-
(connection) =>
|
|
124
|
+
(connection) => connection.key === trigger.connectionKey,
|
|
59
125
|
);
|
|
60
126
|
const definition = connectionTriggerDefinitions.find(
|
|
61
127
|
(definition) => definition.type === trigger.type,
|
|
@@ -75,10 +141,41 @@ export function validateTriggerConnections(
|
|
|
75
141
|
}
|
|
76
142
|
|
|
77
143
|
/** @internal */
|
|
78
|
-
export function createWorkflowConnections
|
|
79
|
-
|
|
80
|
-
|
|
144
|
+
export function createWorkflowConnections<TConnections extends WorkflowConnectionDeclarations>(
|
|
145
|
+
notion: Client,
|
|
146
|
+
declarations: TConnections | undefined,
|
|
147
|
+
): WorkflowConnections<TConnections>;
|
|
148
|
+
export function createWorkflowConnections(
|
|
149
|
+
notion: Client,
|
|
150
|
+
declarations: WorkflowConnectionDeclarations | undefined,
|
|
151
|
+
): Record<string, WorkflowConnectionClient> {
|
|
152
|
+
const requirements = normalizeConnectionRequirements(declarations ?? {});
|
|
153
|
+
// Capture this execution's metadata, but do not parse unused bindings.
|
|
81
154
|
const rawBindings = process.env.NOTION_CONNECTION_BINDINGS;
|
|
155
|
+
const clients: Record<string, WorkflowConnectionClient> = {};
|
|
156
|
+
Object.setPrototypeOf(clients, null);
|
|
157
|
+
for (const requirement of requirements) {
|
|
158
|
+
let client: WorkflowConnectionClient | undefined;
|
|
159
|
+
Object.defineProperty(clients, requirement.key, {
|
|
160
|
+
enumerable: true,
|
|
161
|
+
get() {
|
|
162
|
+
if (client) return client;
|
|
163
|
+
const binding = resolveBinding(requirement.key);
|
|
164
|
+
if (binding.type !== requirement.type) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Workflow connection "${requirement.key}" is ${binding.type}, not ${requirement.type}.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
client =
|
|
170
|
+
requirement.type === "oauth"
|
|
171
|
+
? createOAuthConnection(binding)
|
|
172
|
+
: createConnectionClient(requirement.type, invoker(binding));
|
|
173
|
+
return client;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return Object.freeze(clients);
|
|
178
|
+
|
|
82
179
|
function resolveBinding(key: string): ConnectionBinding {
|
|
83
180
|
const bindings: unknown = JSON.parse(rawBindings ?? "[]");
|
|
84
181
|
if (!Array.isArray(bindings) || !bindings.every(isConnectionBinding)) {
|
|
@@ -92,11 +189,7 @@ export function createWorkflowConnections(notion: Client): WorkflowConnections {
|
|
|
92
189
|
}
|
|
93
190
|
return { ...matches[0] };
|
|
94
191
|
}
|
|
95
|
-
function invoker(
|
|
96
|
-
const binding = resolveBinding(key);
|
|
97
|
-
if (binding.type !== provider) {
|
|
98
|
-
throw new Error(`Workflow connection "${key}" is ${binding.type}, not ${provider}.`);
|
|
99
|
-
}
|
|
192
|
+
function invoker(binding: ConnectionBinding): ConnectionActionInvoker {
|
|
100
193
|
return async <Result>(action: string, args: object): Promise<Result> => {
|
|
101
194
|
const response = await notion.request<{ result: Result }>({
|
|
102
195
|
path: "tools/run/internal",
|
|
@@ -106,7 +199,20 @@ export function createWorkflowConnections(notion: Client): WorkflowConnections {
|
|
|
106
199
|
return response.result;
|
|
107
200
|
};
|
|
108
201
|
}
|
|
109
|
-
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createOAuthConnection(binding: ConnectionBinding): OAuthConnection {
|
|
205
|
+
const envKey = `NOTION_OAUTH_CONNECTION_${binding.id.replaceAll("-", "").toUpperCase()}`;
|
|
206
|
+
const token = process.env[envKey];
|
|
207
|
+
return {
|
|
208
|
+
async accessToken() {
|
|
209
|
+
if (!token)
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Workflow OAuth connection "${binding.key}" requires authorization.`,
|
|
212
|
+
);
|
|
213
|
+
return token;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
110
216
|
}
|
|
111
217
|
|
|
112
218
|
function isConnectionBinding(value: unknown): value is ConnectionBinding {
|
package/src/index.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { customAgent } from "./notion-as-code/custom-agent.js";
|
|
|
3
3
|
export { database } from "./notion-as-code/database.js";
|
|
4
4
|
export { page } from "./notion-as-code/page.js";
|
|
5
5
|
export { teamspace } from "./notion-as-code/teamspace.js";
|
|
6
|
+
export { view } from "./notion-as-code/view.js";
|
|
6
7
|
export { sync } from "./sync.js";
|
|
7
8
|
export { workflow } from "./workflow.js";
|
|
@@ -12,6 +12,7 @@ import { createPage, type ChildPageArgs, type PageHandle } from "./page.js";
|
|
|
12
12
|
import { recordIntent } from "./recorder.js";
|
|
13
13
|
import { assertParent, assertUserResourceId } from "./resource.js";
|
|
14
14
|
import { createAppsDatabase, type AppsSchemaForProperties } from "./schema.js";
|
|
15
|
+
import type { ViewSchema } from "./views.js";
|
|
15
16
|
|
|
16
17
|
/** Input for declaring a database and its optional data sources. */
|
|
17
18
|
export type DatabaseArgs<
|
|
@@ -21,8 +22,7 @@ export type DatabaseArgs<
|
|
|
21
22
|
/** Omit to create a private top-level database in the Apps workspace. */
|
|
22
23
|
parent?: Parent;
|
|
23
24
|
dataSources?: DataSources;
|
|
24
|
-
|
|
25
|
-
views?: readonly unknown[];
|
|
25
|
+
views?: readonly ViewSchema[];
|
|
26
26
|
icon?: NotionAsCodeIcon;
|
|
27
27
|
// TODO: Port cover types.
|
|
28
28
|
cover?: unknown;
|
|
@@ -81,7 +81,7 @@ export type DatabaseHandle<
|
|
|
81
81
|
DataSourcePropertiesForResourceId<DataSources, ResourceId>
|
|
82
82
|
>;
|
|
83
83
|
};
|
|
84
|
-
addView: (view:
|
|
84
|
+
addView: (view: ViewSchema) => void;
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
/** A database whose sole data source is available without an ID lookup. */
|
|
@@ -4,12 +4,14 @@ import { date, datetime } from "./date.js";
|
|
|
4
4
|
import { page } from "./page.js";
|
|
5
5
|
import { assertUserResourceId } from "./resource.js";
|
|
6
6
|
import { teamspace } from "./teamspace.js";
|
|
7
|
+
import { view } from "./view.js";
|
|
7
8
|
|
|
8
9
|
const notionAsCodeFacade = {
|
|
9
10
|
teamspace,
|
|
10
11
|
database,
|
|
11
12
|
page,
|
|
12
13
|
customAgent,
|
|
14
|
+
view,
|
|
13
15
|
text: (value: string) => [[value]],
|
|
14
16
|
number: (value: number) => [[String(value)]],
|
|
15
17
|
date,
|
|
@@ -15,6 +15,61 @@ export type {
|
|
|
15
15
|
} from "./database.js";
|
|
16
16
|
export type { ChildPageArgs, PageArgs, PageHandle } from "./page.js";
|
|
17
17
|
export type { TeamspaceArgs, TeamspaceHandle } from "./teamspace.js";
|
|
18
|
+
export type { ViewArgs, ViewHandle } from "./view.js";
|
|
19
|
+
export type {
|
|
20
|
+
AdvancedFilterSchema,
|
|
21
|
+
BasePropertyFilter,
|
|
22
|
+
BaseViewSchema,
|
|
23
|
+
BoardViewSchema,
|
|
24
|
+
CalendarViewSchema,
|
|
25
|
+
CheckboxGroupByFormat,
|
|
26
|
+
CheckboxPropertyFilter,
|
|
27
|
+
CoverAspectFormat,
|
|
28
|
+
CoverFormat,
|
|
29
|
+
CoverSizeFormat,
|
|
30
|
+
DatabaseViewSortDirection,
|
|
31
|
+
DateFilterMode,
|
|
32
|
+
DateGroupByFormat,
|
|
33
|
+
DatePropertyFilter,
|
|
34
|
+
DatePropertyFilterValue,
|
|
35
|
+
DatePropertyTypes,
|
|
36
|
+
ExactDatePropertyFilterValue,
|
|
37
|
+
ExactRangeDatePropertyFilterValue,
|
|
38
|
+
FeedViewSchema,
|
|
39
|
+
FilterSchema,
|
|
40
|
+
FormulaGroupByFormat,
|
|
41
|
+
GalleryViewSchema,
|
|
42
|
+
GroupByFormat,
|
|
43
|
+
GroupByFormatBase,
|
|
44
|
+
GroupFormat,
|
|
45
|
+
ListViewSchema,
|
|
46
|
+
LocationGroupByFormat,
|
|
47
|
+
MultiSelectPropertyFilter,
|
|
48
|
+
NumberGroupByFormat,
|
|
49
|
+
NumberPropertyFilter,
|
|
50
|
+
PersonGroupByFormat,
|
|
51
|
+
PersonPropertyFilter,
|
|
52
|
+
PropertyFilterSchema,
|
|
53
|
+
PropertyFormat,
|
|
54
|
+
PropertyViewSortSchema,
|
|
55
|
+
PropertyVisibility,
|
|
56
|
+
RelationGroupByFormat,
|
|
57
|
+
RelationPropertyFilter,
|
|
58
|
+
RelativeDatePreset,
|
|
59
|
+
RelativeDatePropertyFilterValue,
|
|
60
|
+
RelativeDateRangeUnit,
|
|
61
|
+
RelativeToTodayDatePropertyFilterValue,
|
|
62
|
+
SelectGroupByFormat,
|
|
63
|
+
SelectPropertyFilter,
|
|
64
|
+
StatusGroupByFormat,
|
|
65
|
+
StatusPropertyFilter,
|
|
66
|
+
TableViewSchema,
|
|
67
|
+
TextGroupByFormat,
|
|
68
|
+
TextPropertyFilter,
|
|
69
|
+
TimelineViewSchema,
|
|
70
|
+
ViewSchema,
|
|
71
|
+
ViewType,
|
|
72
|
+
} from "./views.js";
|
|
18
73
|
|
|
19
74
|
/** Experimental Notion-as-Code facade for Apps SDK project metadata. */
|
|
20
75
|
export const notion = createNotion();
|