@notionhq/apps 0.0.1
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/LICENSE.md +21 -0
- package/README.md +30 -0
- package/dist/cli/build.d.ts +13 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +36 -0
- package/dist/cli/build.test.d.ts +2 -0
- package/dist/cli/build.test.d.ts.map +1 -0
- package/dist/cli/codegen.d.ts +10 -0
- package/dist/cli/codegen.d.ts.map +1 -0
- package/dist/cli/codegen.js +41 -0
- package/dist/cli/codegen.test.d.ts +2 -0
- package/dist/cli/codegen.test.d.ts.map +1 -0
- package/dist/cli/discover.d.ts +32 -0
- package/dist/cli/discover.d.ts.map +1 -0
- package/dist/cli/discover.js +37 -0
- package/dist/cli/discover.test.d.ts +2 -0
- package/dist/cli/discover.test.d.ts.map +1 -0
- package/dist/cli/emit-manifest.d.ts +10 -0
- package/dist/cli/emit-manifest.d.ts.map +1 -0
- package/dist/cli/emit-manifest.js +131 -0
- package/dist/cli/emit-manifest.test.d.ts +2 -0
- package/dist/cli/emit-manifest.test.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +36 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +30 -0
- package/dist/error.d.ts +8 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +11 -0
- package/dist/manifest.d.ts +25 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +0 -0
- package/dist/output.d.ts +7 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +8 -0
- package/dist/runtime-metadata.d.ts +9 -0
- package/dist/runtime-metadata.d.ts.map +1 -0
- package/dist/runtime-metadata.js +33 -0
- package/dist/runtime-metadata.test.d.ts +2 -0
- package/dist/runtime-metadata.test.d.ts.map +1 -0
- package/dist/triggers.d.ts +365 -0
- package/dist/triggers.d.ts.map +1 -0
- package/dist/triggers.generated.d.ts +246 -0
- package/dist/triggers.generated.d.ts.map +1 -0
- package/dist/triggers.generated.js +239 -0
- package/dist/triggers.js +0 -0
- package/dist/workflow.d.ts +125 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +210 -0
- package/dist/workflow.test.d.ts +2 -0
- package/dist/workflow.test.d.ts.map +1 -0
- package/docs/BUILD.md +66 -0
- package/package.json +74 -0
- package/src/cli/build.test.ts +73 -0
- package/src/cli/build.ts +57 -0
- package/src/cli/codegen.test.ts +21 -0
- package/src/cli/codegen.ts +51 -0
- package/src/cli/discover.test.ts +26 -0
- package/src/cli/discover.ts +73 -0
- package/src/cli/emit-manifest.test.ts +45 -0
- package/src/cli/emit-manifest.ts +190 -0
- package/src/cli/index.ts +41 -0
- package/src/context.ts +49 -0
- package/src/error.ts +12 -0
- package/src/manifest.ts +26 -0
- package/src/output.ts +8 -0
- package/src/runtime-metadata.test.ts +54 -0
- package/src/runtime-metadata.ts +46 -0
- package/src/triggers.generated.ts +489 -0
- package/src/triggers.ts +500 -0
- package/src/workflow.test.ts +788 -0
- package/src/workflow.ts +418 -0
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { CapabilityContext } from "./context.js";
|
|
6
|
+
import { createCapabilityContext } from "./context.js";
|
|
7
|
+
import { ExecutionError } from "./error.js";
|
|
8
|
+
import { writeOutput } from "./output.js";
|
|
9
|
+
import { readRunMetadata, type RunMetadata } from "./runtime-metadata.js";
|
|
10
|
+
import type { WorkflowEventMap, WorkflowTrigger } from "./triggers.generated.js";
|
|
11
|
+
|
|
12
|
+
type HandlerOptions = {
|
|
13
|
+
/** Return the concrete result instead of writing the output envelope. */
|
|
14
|
+
concreteOutput?: true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
|
|
18
|
+
|
|
19
|
+
export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
|
|
20
|
+
|
|
21
|
+
export type WorkflowEventForTriggers<T extends readonly WorkflowTrigger[]> =
|
|
22
|
+
WorkflowEventForTrigger<T[number]>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configuration passed to {@link createWorkflow}.
|
|
26
|
+
*/
|
|
27
|
+
export type WorkflowConfiguration<
|
|
28
|
+
TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
|
|
29
|
+
> = {
|
|
30
|
+
/**
|
|
31
|
+
* A human-readable name for the workflow, shown in the UI when viewing workflows
|
|
32
|
+
*/
|
|
33
|
+
name: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A human-readable description of what the workflow does, shown in the UI when viewing workflows
|
|
37
|
+
*/
|
|
38
|
+
description: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An array of triggers that can invoke this workflow.
|
|
42
|
+
*
|
|
43
|
+
* Each trigger defines a specific event or condition that causes the workflow to run.
|
|
44
|
+
*/
|
|
45
|
+
triggers: TTriggers;
|
|
46
|
+
|
|
47
|
+
handler: (
|
|
48
|
+
event: WorkflowEventForTriggers<TTriggers>,
|
|
49
|
+
context: WorkflowContext,
|
|
50
|
+
) => Promise<void> | void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A workflow capability as returned by {@link createWorkflow}.
|
|
55
|
+
*
|
|
56
|
+
* Note that a workflow carries no key of its own — the build tool derives
|
|
57
|
+
* the capability key from the file the workflow is default-exported from.
|
|
58
|
+
*/
|
|
59
|
+
export type Workflow<
|
|
60
|
+
TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]] = readonly [
|
|
61
|
+
WorkflowTrigger,
|
|
62
|
+
...WorkflowTrigger[],
|
|
63
|
+
],
|
|
64
|
+
> = {
|
|
65
|
+
_tag: "workflow";
|
|
66
|
+
config: {
|
|
67
|
+
name: string;
|
|
68
|
+
description: string;
|
|
69
|
+
triggers: TTriggers;
|
|
70
|
+
};
|
|
71
|
+
handler: (
|
|
72
|
+
event: WorkflowEventForTriggers<TTriggers>,
|
|
73
|
+
options?: HandlerOptions,
|
|
74
|
+
) => Promise<{ status: "success" } | undefined>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a workflow capability.
|
|
79
|
+
*
|
|
80
|
+
* Default-export the result from a file directly under `src/workflows/` —
|
|
81
|
+
* the file's basename becomes the workflow's key.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // src/workflows/onPageCreated.ts
|
|
86
|
+
* import { triggers } from "@notionhq/apps/triggers";
|
|
87
|
+
* import { createWorkflow } from "@notionhq/apps/workflow";
|
|
88
|
+
*
|
|
89
|
+
* export default createWorkflow({
|
|
90
|
+
* name: "Send Welcome Email",
|
|
91
|
+
* description: "Sends a welcome email when a new page is added",
|
|
92
|
+
* triggers: [triggers.notionPageCreated()],
|
|
93
|
+
* handler: async (event, context) => {
|
|
94
|
+
* console.log(event.page);
|
|
95
|
+
* await context.step("Process page", async ({ id }) => {
|
|
96
|
+
* console.log(`Running step ${id}`);
|
|
97
|
+
* return processPage(event.page);
|
|
98
|
+
* });
|
|
99
|
+
* },
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function createWorkflow<
|
|
104
|
+
const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
|
|
105
|
+
>(configuration: WorkflowConfiguration<TTriggers>): Workflow<TTriggers> {
|
|
106
|
+
return {
|
|
107
|
+
_tag: "workflow",
|
|
108
|
+
config: {
|
|
109
|
+
name: configuration.name,
|
|
110
|
+
description: configuration.description,
|
|
111
|
+
triggers: configuration.triggers,
|
|
112
|
+
},
|
|
113
|
+
async handler(
|
|
114
|
+
event: WorkflowEventForTriggers<TTriggers>,
|
|
115
|
+
options?: HandlerOptions,
|
|
116
|
+
): Promise<{ status: "success" } | undefined> {
|
|
117
|
+
try {
|
|
118
|
+
const runMetadata = readRunMetadata();
|
|
119
|
+
const capabilityContext: WorkflowContext = {
|
|
120
|
+
...createCapabilityContext(),
|
|
121
|
+
...runMetadata,
|
|
122
|
+
step: createStep(runMetadata),
|
|
123
|
+
};
|
|
124
|
+
await configuration.handler(event, capabilityContext);
|
|
125
|
+
|
|
126
|
+
if (options?.concreteOutput) {
|
|
127
|
+
return { status: "success" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
writeOutput({ _tag: "success", value: { status: "success" } });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const error = new ExecutionError(err);
|
|
133
|
+
|
|
134
|
+
if (!options?.concreteOutput) {
|
|
135
|
+
writeOutput({
|
|
136
|
+
_tag: "error",
|
|
137
|
+
error: {
|
|
138
|
+
name: error.name,
|
|
139
|
+
message: error.message,
|
|
140
|
+
trace: error.stack,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Context passed to a workflow step. */
|
|
152
|
+
export type StepContext = {
|
|
153
|
+
/**
|
|
154
|
+
* An identifier that is stable for this step across attempts of the workflow run.
|
|
155
|
+
* Use this to make side effects idempotent when the workflow is retried.
|
|
156
|
+
*/
|
|
157
|
+
id: string;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
type WorkflowStepResult<T> = T extends void ? null : T;
|
|
161
|
+
|
|
162
|
+
/** Options for a workflow step. */
|
|
163
|
+
export type WorkflowStepOptions = {
|
|
164
|
+
/**
|
|
165
|
+
* A stable key used to identify this step across attempts of a workflow run.
|
|
166
|
+
* Pass a string for a single key segment or an array of strings for a composite
|
|
167
|
+
* key. Defaults to the step name. Changing the key creates a new logical step.
|
|
168
|
+
*/
|
|
169
|
+
key: string | string[];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type WorkflowStep = {
|
|
173
|
+
<T>(name: string, fn: (context: StepContext) => T | Promise<T>): Promise<WorkflowStepResult<T>>;
|
|
174
|
+
<T>(
|
|
175
|
+
name: string,
|
|
176
|
+
options: WorkflowStepOptions,
|
|
177
|
+
fn: (context: StepContext) => T | Promise<T>,
|
|
178
|
+
): Promise<WorkflowStepResult<T>>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** Context passed to a workflow handler. */
|
|
182
|
+
export type WorkflowContext = CapabilityContext &
|
|
183
|
+
RunMetadata & {
|
|
184
|
+
/**
|
|
185
|
+
* Run an observable unit of work within the workflow.
|
|
186
|
+
*
|
|
187
|
+
* The callback may be synchronous or asynchronous, but `step` always returns
|
|
188
|
+
* a promise. Its return value must be JSON-serializable so it can be included
|
|
189
|
+
* in the success event written to stdout. A callback that returns `undefined`
|
|
190
|
+
* is normalized to `null`, which keeps first runs and saved replays consistent.
|
|
191
|
+
*
|
|
192
|
+
* The step name is used to derive its stable replay key by default. Step key
|
|
193
|
+
* inputs must be unique within a workflow invocation. Pass an explicit string
|
|
194
|
+
* or array of strings when display names are duplicated or may change.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const page = await context.step("Fetch page", async ({ id }) => {
|
|
199
|
+
* console.log(`Running step ${id}`);
|
|
200
|
+
* return context.notion.pages.retrieve({ page_id: pageId });
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
step: WorkflowStep;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
type StepCompletionType = "success" | "failure";
|
|
208
|
+
type StepEventType = "started" | "success" | "failure" | "replayed" | "completed";
|
|
209
|
+
|
|
210
|
+
type WorkflowStepCheckpoint = {
|
|
211
|
+
version: 0;
|
|
212
|
+
key: string;
|
|
213
|
+
value: unknown;
|
|
214
|
+
sourceRunId: string;
|
|
215
|
+
sourceStepId: string;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const WORKFLOW_STEP_DIRECTORY_ENV_KEY = "NOTION_WORKFLOW_STEP_DIRECTORY";
|
|
219
|
+
const MAX_WORKFLOW_STEP_KEY_LENGTH = 43;
|
|
220
|
+
const WORKFLOW_STEP_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
221
|
+
const WORKFLOW_STEP_KEY_HASH_DOMAIN = "notion-workflow-step-key";
|
|
222
|
+
const WORKFLOW_STEP_KEY_HASH_VERSION = 1;
|
|
223
|
+
|
|
224
|
+
function writeStepEvent(
|
|
225
|
+
event: StepEventType,
|
|
226
|
+
value: Record<string, unknown>,
|
|
227
|
+
startedAt: number,
|
|
228
|
+
emittedAt = performance.now(),
|
|
229
|
+
) {
|
|
230
|
+
const tag = `__notion_step_${event}__`;
|
|
231
|
+
process.stdout.write(
|
|
232
|
+
`\n<${tag}>${JSON.stringify({
|
|
233
|
+
...value,
|
|
234
|
+
timestampMs: performance.timeOrigin + emittedAt,
|
|
235
|
+
durationMs: emittedAt - startedAt,
|
|
236
|
+
})}</${tag}>\n`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function createStep({ runGroupId }: Pick<RunMetadata, "runGroupId">): WorkflowContext["step"] {
|
|
241
|
+
const stepNameByKey = new Map<string, string>();
|
|
242
|
+
|
|
243
|
+
async function step<T>(
|
|
244
|
+
name: string,
|
|
245
|
+
optionsOrFn: WorkflowStepOptions | ((context: StepContext) => T | Promise<T>),
|
|
246
|
+
maybeFn?: (context: StepContext) => T | Promise<T>,
|
|
247
|
+
): Promise<WorkflowStepResult<T>> {
|
|
248
|
+
const options = typeof optionsOrFn === "function" ? undefined : optionsOrFn;
|
|
249
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
250
|
+
if (!fn) {
|
|
251
|
+
throw new Error(`Workflow step "${name}" is missing its callback.`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const keyInput = options?.key ?? name;
|
|
255
|
+
const key = createWorkflowStepKey(typeof keyInput === "string" ? [keyInput] : keyInput);
|
|
256
|
+
const existingStepName = stepNameByKey.get(key);
|
|
257
|
+
if (existingStepName !== undefined) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Workflow step "${name}" has the same key as "${existingStepName}". Step names are used as keys by default; provide a unique name or an explicit key.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
stepNameByKey.set(key, name);
|
|
263
|
+
|
|
264
|
+
const context: StepContext = { id: `${runGroupId}:${key}` };
|
|
265
|
+
const event = { id: context.id, name, key };
|
|
266
|
+
const startedAt = performance.now();
|
|
267
|
+
|
|
268
|
+
writeStepEvent("started", event, startedAt, startedAt);
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const checkpoint = await readWorkflowStepCheckpoint(key);
|
|
272
|
+
if (checkpoint) {
|
|
273
|
+
writeStepEvent(
|
|
274
|
+
"replayed",
|
|
275
|
+
{
|
|
276
|
+
...event,
|
|
277
|
+
sourceRunId: checkpoint.sourceRunId,
|
|
278
|
+
sourceStepId: checkpoint.sourceStepId,
|
|
279
|
+
},
|
|
280
|
+
startedAt,
|
|
281
|
+
);
|
|
282
|
+
writeStepEvent(
|
|
283
|
+
"completed",
|
|
284
|
+
{
|
|
285
|
+
...event,
|
|
286
|
+
type: "success" satisfies StepCompletionType,
|
|
287
|
+
},
|
|
288
|
+
startedAt,
|
|
289
|
+
);
|
|
290
|
+
return checkpoint.value as WorkflowStepResult<T>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const value = normalizeWorkflowStepResult(await fn(context));
|
|
294
|
+
writeStepEvent("success", { ...event, value }, startedAt);
|
|
295
|
+
writeStepEvent(
|
|
296
|
+
"completed",
|
|
297
|
+
{
|
|
298
|
+
...event,
|
|
299
|
+
type: "success" satisfies StepCompletionType,
|
|
300
|
+
},
|
|
301
|
+
startedAt,
|
|
302
|
+
);
|
|
303
|
+
return value;
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const error = new ExecutionError(err);
|
|
306
|
+
writeStepEvent(
|
|
307
|
+
"failure",
|
|
308
|
+
{
|
|
309
|
+
...event,
|
|
310
|
+
error: {
|
|
311
|
+
name: error.name,
|
|
312
|
+
message: error.message,
|
|
313
|
+
trace: error.stack,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
startedAt,
|
|
317
|
+
);
|
|
318
|
+
writeStepEvent(
|
|
319
|
+
"completed",
|
|
320
|
+
{
|
|
321
|
+
...event,
|
|
322
|
+
type: "failure" satisfies StepCompletionType,
|
|
323
|
+
},
|
|
324
|
+
startedAt,
|
|
325
|
+
);
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return step;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function createWorkflowStepKey(segments: readonly unknown[]): string {
|
|
334
|
+
if (segments.length === 0) {
|
|
335
|
+
throw new Error("Workflow step keys must contain at least one segment.");
|
|
336
|
+
}
|
|
337
|
+
for (const segment of segments) {
|
|
338
|
+
if (typeof segment !== "string") {
|
|
339
|
+
throw new Error("Workflow step key segments must be strings.");
|
|
340
|
+
}
|
|
341
|
+
if (segment.length === 0) {
|
|
342
|
+
throw new Error("Workflow step key segments must not be empty.");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const serialized = JSON.stringify([
|
|
347
|
+
WORKFLOW_STEP_KEY_HASH_DOMAIN,
|
|
348
|
+
WORKFLOW_STEP_KEY_HASH_VERSION,
|
|
349
|
+
...segments,
|
|
350
|
+
]);
|
|
351
|
+
const key = createHash("sha256").update(serialized).digest("base64url");
|
|
352
|
+
if (key.length !== MAX_WORKFLOW_STEP_KEY_LENGTH || !WORKFLOW_STEP_KEY_PATTERN.test(key)) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`Workflow step keys must be ${MAX_WORKFLOW_STEP_KEY_LENGTH} Base64url characters after hashing.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return key;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizeWorkflowStepResult<T>(value: T): WorkflowStepResult<T> {
|
|
362
|
+
return (value === undefined ? null : value) as WorkflowStepResult<T>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function readWorkflowStepCheckpoint(
|
|
366
|
+
key: string,
|
|
367
|
+
): Promise<WorkflowStepCheckpoint | undefined> {
|
|
368
|
+
const directory = process.env[WORKFLOW_STEP_DIRECTORY_ENV_KEY];
|
|
369
|
+
if (!directory) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// The key may contain slashes, private data, or more bytes than a file name
|
|
374
|
+
// allows. Hashing gives both sides a safe path while the key inside the file
|
|
375
|
+
// still proves that this checkpoint belongs to the requested step.
|
|
376
|
+
const path = join(directory, `${hashWorkflowStepKey(key)}.json`);
|
|
377
|
+
let serialized: string;
|
|
378
|
+
try {
|
|
379
|
+
serialized = await readFile(path, "utf8");
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const parsed: unknown = JSON.parse(serialized);
|
|
388
|
+
if (
|
|
389
|
+
!isRecord(parsed) ||
|
|
390
|
+
parsed.version !== 0 ||
|
|
391
|
+
parsed.key !== key ||
|
|
392
|
+
!Object.hasOwn(parsed, "value") ||
|
|
393
|
+
typeof parsed.sourceRunId !== "string" ||
|
|
394
|
+
typeof parsed.sourceStepId !== "string"
|
|
395
|
+
) {
|
|
396
|
+
throw new Error(`Workflow step checkpoint ${hashWorkflowStepKey(key)} is invalid.`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
version: 0,
|
|
401
|
+
key,
|
|
402
|
+
value: parsed.value,
|
|
403
|
+
sourceRunId: parsed.sourceRunId,
|
|
404
|
+
sourceStepId: parsed.sourceStepId,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hashWorkflowStepKey(key: string): string {
|
|
409
|
+
return createHash("sha256").update(key).digest("hex");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
413
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
417
|
+
return error instanceof Error && "code" in error;
|
|
418
|
+
}
|