@notionhq/apps 0.0.17 → 0.0.19
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 +77 -35
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +12 -7
- package/dist/cli/emit-manifest.d.ts +1 -1
- package/dist/cli/emit-manifest.d.ts.map +1 -1
- package/dist/cli/emit-manifest.js +66 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/notion-as-code/custom-agent.d.ts +1 -0
- package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
- package/dist/notion-as-code/custom-agent.js +1 -1
- package/dist/notion-as-code/database.d.ts +90 -48
- package/dist/notion-as-code/database.d.ts.map +1 -1
- package/dist/notion-as-code/database.js +135 -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/index.d.ts +2 -2
- package/dist/notion-as-code/index.d.ts.map +1 -1
- package/dist/notion-as-code/intents.d.ts +34 -2
- package/dist/notion-as-code/intents.d.ts.map +1 -1
- package/dist/notion-as-code/page.d.ts +1 -0
- package/dist/notion-as-code/page.d.ts.map +1 -1
- package/dist/notion-as-code/page.js +1 -0
- 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/views.d.ts +29 -10
- package/dist/notion-as-code/views.d.ts.map +1 -1
- package/dist/sync.d.ts +33 -15
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +12 -0
- package/dist/workflow-access-types.test.d.ts +2 -0
- package/dist/workflow-access-types.test.d.ts.map +1 -0
- package/dist/workflow-access.d.ts +68 -0
- package/dist/workflow-access.d.ts.map +1 -0
- package/dist/workflow-access.js +135 -0
- 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 +65 -9
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +119 -5
- package/docs/BUILD.md +134 -4
- package/package.json +1 -1
- package/skills/notion-as-code/SKILL.md +89 -49
- package/skills/sync/SKILL.md +21 -18
- package/skills/workflow/SKILL.md +101 -17
- package/src/cli/build.test.ts +148 -64
- package/src/cli/build.ts +13 -8
- package/src/cli/emit-manifest.ts +107 -0
- package/src/index.ts +21 -1
- package/src/notion-as-code/custom-agent.ts +2 -1
- package/src/notion-as-code/database.test.ts +661 -0
- package/src/notion-as-code/database.ts +349 -127
- package/src/notion-as-code/index.ts +16 -1
- package/src/notion-as-code/intents.ts +39 -2
- package/src/notion-as-code/page.ts +2 -0
- package/src/notion-as-code/schema.ts +11 -3
- package/src/notion-as-code/views.ts +29 -10
- package/src/sync.test.ts +295 -0
- package/src/sync.ts +85 -21
- package/src/workflow-access-types.test.ts +69 -0
- package/src/workflow-access.ts +299 -0
- package/src/workflow-state.ts +152 -0
- package/src/workflow.test.ts +441 -1
- package/src/workflow.ts +230 -13
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { CustomAgentHandle } from "./notion-as-code/custom-agent.js";
|
|
2
|
+
import type {
|
|
3
|
+
DataSourceHandle,
|
|
4
|
+
DatabaseHandle,
|
|
5
|
+
SingleSourceDatabaseHandle,
|
|
6
|
+
} from "./notion-as-code/database.js";
|
|
7
|
+
import type { PageHandle } from "./notion-as-code/page.js";
|
|
8
|
+
|
|
9
|
+
/** A Notion-as-Code resource that can provide a named workflow binding. */
|
|
10
|
+
export type WorkflowAccessResource =
|
|
11
|
+
| PageHandle
|
|
12
|
+
| DatabaseHandle
|
|
13
|
+
| SingleSourceDatabaseHandle
|
|
14
|
+
| DataSourceHandle
|
|
15
|
+
| CustomAgentHandle;
|
|
16
|
+
|
|
17
|
+
/** Resource kinds accepted by workflow access declarations. */
|
|
18
|
+
export type WorkflowAccessResourceType = WorkflowAccessResource["resourceType"];
|
|
19
|
+
|
|
20
|
+
/** Levels accepted by pages, databases, and data sources. */
|
|
21
|
+
export type WorkflowAccessResourceLevel = "view" | "comment" | "edit" | "fullAccess";
|
|
22
|
+
|
|
23
|
+
/** The only level accepted by a custom agent. */
|
|
24
|
+
export type WorkflowAccessAgentLevel = "call";
|
|
25
|
+
|
|
26
|
+
/** Every level accepted by at least one workflow access resource. */
|
|
27
|
+
export type WorkflowAccessLevel = WorkflowAccessResourceLevel | WorkflowAccessAgentLevel;
|
|
28
|
+
|
|
29
|
+
/** The level set accepted by a particular resource kind. */
|
|
30
|
+
export type WorkflowAccessLevelFor<TType extends WorkflowAccessResourceType> =
|
|
31
|
+
TType extends "customAgent" ? WorkflowAccessAgentLevel : WorkflowAccessResourceLevel;
|
|
32
|
+
|
|
33
|
+
type AccessResourceFor<TType extends WorkflowAccessResourceType> = Extract<
|
|
34
|
+
WorkflowAccessResource,
|
|
35
|
+
{ resourceType: TType }
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
/** A named binding declaration pairing one NaC handle with its allowed level. */
|
|
39
|
+
export type WorkflowAccessDeclaration = {
|
|
40
|
+
[TType in WorkflowAccessResourceType]: {
|
|
41
|
+
readonly resource: AccessResourceFor<TType>;
|
|
42
|
+
readonly level: WorkflowAccessLevelFor<TType>;
|
|
43
|
+
};
|
|
44
|
+
}[WorkflowAccessResourceType];
|
|
45
|
+
|
|
46
|
+
/** Named binding declarations keyed by the names exposed on workflow context. */
|
|
47
|
+
export type WorkflowAccessDeclarations = Readonly<Record<string, WorkflowAccessDeclaration>>;
|
|
48
|
+
|
|
49
|
+
/** A normalized named-binding requirement, including the required NaC resourceId. */
|
|
50
|
+
export type WorkflowAccessRequirement = {
|
|
51
|
+
[TType in WorkflowAccessResourceType]: {
|
|
52
|
+
readonly type: TType;
|
|
53
|
+
readonly resourceId: string;
|
|
54
|
+
readonly level: WorkflowAccessLevelFor<TType>;
|
|
55
|
+
};
|
|
56
|
+
}[WorkflowAccessResourceType];
|
|
57
|
+
|
|
58
|
+
/** Normalized named-binding requirements keyed by public alias. */
|
|
59
|
+
export type WorkflowAccessRequirements = Readonly<Record<string, WorkflowAccessRequirement>>;
|
|
60
|
+
|
|
61
|
+
/** The resolved NaC binding exposed to a workflow handler. */
|
|
62
|
+
export type WorkflowAccessValue<
|
|
63
|
+
TType extends WorkflowAccessResourceType = WorkflowAccessResourceType,
|
|
64
|
+
> = Readonly<{
|
|
65
|
+
type: TType;
|
|
66
|
+
id: string;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
69
|
+
/** Resolved access values keyed by their public aliases. */
|
|
70
|
+
export type WorkflowAccess<
|
|
71
|
+
TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
|
|
72
|
+
> = Readonly<{
|
|
73
|
+
[TKey in keyof TAccess]: TAccess[TKey] extends {
|
|
74
|
+
readonly resource: { readonly resourceType: infer TType };
|
|
75
|
+
}
|
|
76
|
+
? TType extends WorkflowAccessResourceType
|
|
77
|
+
? WorkflowAccessValue<TType>
|
|
78
|
+
: never
|
|
79
|
+
: never;
|
|
80
|
+
}>;
|
|
81
|
+
|
|
82
|
+
/** Resolved NaC bindings keyed by public aliases, without declaration metadata. */
|
|
83
|
+
export type WorkflowAccessBindings = Readonly<Record<string, WorkflowAccessValue>>;
|
|
84
|
+
|
|
85
|
+
const ACCESS_ALIAS_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;
|
|
86
|
+
const RESERVED_ACCESS_ALIASES = new Set<string>(["__proto__", "prototype", "constructor"]);
|
|
87
|
+
const RESOURCE_TYPES: Record<WorkflowAccessResourceType, true> = {
|
|
88
|
+
page: true,
|
|
89
|
+
database: true,
|
|
90
|
+
dataSource: true,
|
|
91
|
+
customAgent: true,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalize public binding declarations without retaining the handle objects.
|
|
96
|
+
* Handles contain methods and schema objects, so they must never be cloned into
|
|
97
|
+
* the manifest or captured by the runtime transport. These declarations do not
|
|
98
|
+
* grant or change the workflow's existing Notion-module permissions.
|
|
99
|
+
*/
|
|
100
|
+
export function normalizeWorkflowAccess(
|
|
101
|
+
declarations: WorkflowAccessDeclarations | undefined,
|
|
102
|
+
): WorkflowAccessRequirements {
|
|
103
|
+
const source = declarations ?? {};
|
|
104
|
+
assertPlainObject(source, "Workflow access must be an object keyed by alias.");
|
|
105
|
+
const keys = Object.keys(source);
|
|
106
|
+
if (Reflect.ownKeys(source).length !== keys.length) {
|
|
107
|
+
throw new Error("Workflow access aliases must be enumerable strings.");
|
|
108
|
+
}
|
|
109
|
+
if (keys.length > 100) {
|
|
110
|
+
throw new Error("Workflows support at most 100 access requirements.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const normalized: Record<string, WorkflowAccessRequirement> = Object.create(null) as Record<
|
|
114
|
+
string,
|
|
115
|
+
WorkflowAccessRequirement
|
|
116
|
+
>;
|
|
117
|
+
for (const alias of keys) {
|
|
118
|
+
if (!ACCESS_ALIAS_PATTERN.test(alias) || RESERVED_ACCESS_ALIASES.has(alias)) {
|
|
119
|
+
throw new Error(`Invalid workflow access alias: ${alias}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const declaration: unknown = source[alias];
|
|
123
|
+
if (
|
|
124
|
+
!isRecord(declaration) ||
|
|
125
|
+
!Object.hasOwn(declaration, "resource") ||
|
|
126
|
+
!Object.hasOwn(declaration, "level")
|
|
127
|
+
) {
|
|
128
|
+
throw new Error(`Workflow access "${alias}" must declare a resource and level.`);
|
|
129
|
+
}
|
|
130
|
+
const resource: unknown = declaration.resource;
|
|
131
|
+
if (
|
|
132
|
+
!isRecord(resource) ||
|
|
133
|
+
!Object.hasOwn(resource, "resourceType") ||
|
|
134
|
+
!Object.hasOwn(resource, "resourceId") ||
|
|
135
|
+
typeof resource.resourceId !== "string" ||
|
|
136
|
+
resource.resourceId.length === 0 ||
|
|
137
|
+
!isWorkflowAccessResourceType(resource.resourceType)
|
|
138
|
+
) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Workflow access "${alias}" must reference a supported NaC resource handle.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let requirement: WorkflowAccessRequirement;
|
|
145
|
+
if (resource.resourceType === "customAgent") {
|
|
146
|
+
if (declaration.level !== "call") {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Workflow access "${alias}" custom agents only support level "call".`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
requirement = {
|
|
152
|
+
type: resource.resourceType,
|
|
153
|
+
resourceId: resource.resourceId,
|
|
154
|
+
level: declaration.level,
|
|
155
|
+
};
|
|
156
|
+
} else {
|
|
157
|
+
if (!isWorkflowAccessResourceLevel(declaration.level)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Workflow access "${alias}" ${resource.resourceType} level must be view, comment, edit, or fullAccess.`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
requirement = {
|
|
163
|
+
type: resource.resourceType,
|
|
164
|
+
resourceId: resource.resourceId,
|
|
165
|
+
level: declaration.level,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
normalized[alias] = Object.freeze(requirement);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Object.freeze(normalized);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve trusted per-invocation NaC bindings into the minimal context value.
|
|
176
|
+
* This exposes only declared aliases and does not determine Notion API
|
|
177
|
+
* authorization; the workflow's existing Notion-module permissions do that.
|
|
178
|
+
* The environment is deliberately not parsed when no binding is required.
|
|
179
|
+
*/
|
|
180
|
+
export function hydrateWorkflowAccess<TAccess extends WorkflowAccessDeclarations>(
|
|
181
|
+
requirements: WorkflowAccessRequirements | undefined,
|
|
182
|
+
): WorkflowAccess<TAccess>;
|
|
183
|
+
export function hydrateWorkflowAccess(
|
|
184
|
+
requirements: WorkflowAccessRequirements | undefined,
|
|
185
|
+
): WorkflowAccessBindings {
|
|
186
|
+
const expected = requirements ?? {};
|
|
187
|
+
const aliases = Object.keys(expected);
|
|
188
|
+
const empty: Record<string, WorkflowAccessValue> = Object.create(null) as Record<
|
|
189
|
+
string,
|
|
190
|
+
WorkflowAccessValue
|
|
191
|
+
>;
|
|
192
|
+
if (aliases.length === 0) {
|
|
193
|
+
return Object.freeze(empty);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const rawBindings = process.env.NOTION_ACCESS_BINDINGS;
|
|
197
|
+
if (rawBindings === undefined) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"Workflow access requirements are not bound. Deploy and configure this requirement before running it.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (rawBindings.length === 0) {
|
|
203
|
+
throw new Error("Invalid NOTION_ACCESS_BINDINGS metadata.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let parsed: unknown;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(rawBindings);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error("Invalid NOTION_ACCESS_BINDINGS metadata.", { cause: error });
|
|
211
|
+
}
|
|
212
|
+
assertPlainObject(parsed, "Invalid NOTION_ACCESS_BINDINGS metadata.");
|
|
213
|
+
const bindingKeys = Object.keys(parsed);
|
|
214
|
+
if (
|
|
215
|
+
Reflect.ownKeys(parsed).length !== bindingKeys.length ||
|
|
216
|
+
bindingKeys.length !== aliases.length ||
|
|
217
|
+
bindingKeys.some((alias) => !Object.hasOwn(expected, alias))
|
|
218
|
+
) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
"Invalid NOTION_ACCESS_BINDINGS metadata: aliases do not match workflow access.",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const resolved: Record<string, WorkflowAccessValue> = Object.create(null) as Record<
|
|
225
|
+
string,
|
|
226
|
+
WorkflowAccessValue
|
|
227
|
+
>;
|
|
228
|
+
for (const alias of aliases) {
|
|
229
|
+
const requirement = expected[alias];
|
|
230
|
+
if (!requirement) {
|
|
231
|
+
throw new Error(`Workflow access "${alias}" is not declared.`);
|
|
232
|
+
}
|
|
233
|
+
const binding: unknown = parsed[alias];
|
|
234
|
+
if (!isBinding(binding)) {
|
|
235
|
+
throw new Error(`Invalid NOTION_ACCESS_BINDINGS metadata for access "${alias}".`);
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
binding.type !== requirement.type ||
|
|
239
|
+
binding.resourceId !== requirement.resourceId ||
|
|
240
|
+
binding.level !== requirement.level
|
|
241
|
+
) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Workflow access "${alias}" binding does not match its declared type, resourceId, or level.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
resolved[alias] = Object.freeze({ type: binding.type, id: binding.id });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return Object.freeze(resolved);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isWorkflowAccessResourceType(value: unknown): value is WorkflowAccessResourceType {
|
|
253
|
+
return typeof value === "string" && Object.hasOwn(RESOURCE_TYPES, value);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function isWorkflowAccessResourceLevel(value: unknown): value is WorkflowAccessResourceLevel {
|
|
257
|
+
return value === "view" || value === "comment" || value === "edit" || value === "fullAccess";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isBinding(value: unknown): value is {
|
|
261
|
+
type: WorkflowAccessResourceType;
|
|
262
|
+
resourceId: string;
|
|
263
|
+
level: WorkflowAccessResourceLevel | WorkflowAccessAgentLevel;
|
|
264
|
+
id: string;
|
|
265
|
+
} {
|
|
266
|
+
if (!isRecord(value) || Reflect.ownKeys(value).length !== 4) return false;
|
|
267
|
+
const keys = Object.keys(value);
|
|
268
|
+
if (
|
|
269
|
+
keys.length !== 4 ||
|
|
270
|
+
!keys.includes("type") ||
|
|
271
|
+
!keys.includes("resourceId") ||
|
|
272
|
+
!keys.includes("level") ||
|
|
273
|
+
!keys.includes("id")
|
|
274
|
+
) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return (
|
|
278
|
+
isWorkflowAccessResourceType(value.type) &&
|
|
279
|
+
typeof value.resourceId === "string" &&
|
|
280
|
+
value.resourceId.length > 0 &&
|
|
281
|
+
typeof value.level === "string" &&
|
|
282
|
+
(isWorkflowAccessResourceLevel(value.level) || value.level === "call") &&
|
|
283
|
+
typeof value.id === "string" &&
|
|
284
|
+
value.id.length > 0
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function assertPlainObject(
|
|
289
|
+
value: unknown,
|
|
290
|
+
message: string,
|
|
291
|
+
): asserts value is Record<string, unknown> {
|
|
292
|
+
if (!isRecord(value)) throw new Error(message);
|
|
293
|
+
const prototype = Object.getPrototypeOf(value);
|
|
294
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error(message);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
299
|
+
}
|
|
@@ -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
|
+
}
|