@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/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 {
|
|
7
|
-
import type {
|
|
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:
|
|
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<
|
|
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
|
-
|
|
94
|
-
PrimaryKey extends KeyPropertyName<
|
|
143
|
+
Schema extends NotionAsCodeSchema,
|
|
144
|
+
PrimaryKey extends KeyPropertyName<AppsSchemaForSchema<Schema>>,
|
|
95
145
|
State,
|
|
96
|
-
> =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
170
|
-
const PrimaryKey extends KeyPropertyName<
|
|
217
|
+
const Schema extends NotionAsCodeSchema,
|
|
218
|
+
const PrimaryKey extends KeyPropertyName<AppsSchemaForSchema<Schema>>,
|
|
171
219
|
State = unknown,
|
|
172
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
10
|
+
connections: { work: connections.calendar() },
|
|
11
11
|
handler(_event, context) {
|
|
12
|
-
expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"
|
|
13
|
-
expectTypeOf(context.connections.
|
|
12
|
+
expectTypeOf<keyof typeof context.connections>().toEqualTypeOf<"work">();
|
|
13
|
+
expectTypeOf(context.connections.work.listCalendars).toBeFunction();
|
|
14
14
|
},
|
|
15
15
|
});
|
|
16
|
-
const requirements =
|
|
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
|
|
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<
|
|
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 =
|
|
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<"
|
|
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:
|
|
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: "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
65
|
+
connections: "{ support: connections.calendar() }",
|
|
66
66
|
trigger: '{ type: "slack.message", connectionKey: "support" }',
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
|
-
name: "
|
|
69
|
+
name: "old_key_option",
|
|
70
70
|
valid: false,
|
|
71
|
-
connections: '
|
|
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: '
|
|
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: "
|
|
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: "
|
|
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(
|
|
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
|
);
|