@teamkeel/functions-runtime 0.413.8 → 0.414.0

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/dist/index.d.cts CHANGED
@@ -545,19 +545,31 @@ type UIApiResponses = {
545
545
  };
546
546
  };
547
547
 
548
- interface FlowContext<C extends FlowConfig> {
548
+ interface FlowContext<C extends FlowConfig, E = any, S = any> {
549
549
  step: Step<C>;
550
550
  ui: UI<C>;
551
+ env: E;
552
+ now: Date;
553
+ secrets: S;
551
554
  }
552
555
  type JsonSerializable = string | number | boolean | null | JsonSerializable[] | {
553
556
  [key: string]: JsonSerializable;
554
557
  };
555
558
  type Step<C extends FlowConfig> = {
556
- <R extends JsonSerializable | void>(name: string, options: {
559
+ <R extends JsonSerializable | void>(
560
+ /** The unique name of this step. */
561
+ name: string,
562
+ /** Configuration options for the step. */
563
+ options: {
564
+ /** The stage this step belongs to. Used for organising steps in the UI. */
557
565
  stage?: ExtractStageKeys<C>;
558
- maxRetries?: number;
559
- timeoutInMs?: number;
560
- }, fn: () => Promise<R> & {
566
+ /** Number of times to retry the step if it fails. Defaults to 5. */
567
+ retries?: number;
568
+ /** Maximum time in milliseconds to wait for the step to complete. Defaults to 60000 (1 minute). */
569
+ timeout?: number;
570
+ },
571
+ /** The step function to execute. */
572
+ fn: () => Promise<R> & {
561
573
  catch: (errorHandler: (err: Error) => Promise<void> | void) => Promise<any>;
562
574
  }): Promise<R>;
563
575
  <R extends JsonSerializable | void>(name: string, fn: () => Promise<R> & {
@@ -565,8 +577,11 @@ type Step<C extends FlowConfig> = {
565
577
  }): Promise<R>;
566
578
  };
567
579
  interface FlowConfig {
580
+ /** The stages to organise the steps in the flow. */
568
581
  stages?: StageConfig[];
582
+ /** The title of the flow as shown in the Console. */
569
583
  title?: string;
584
+ /** The description of the flow as shown in the Console. */
570
585
  description?: string;
571
586
  }
572
587
  interface FlowConfigAPI {
@@ -574,20 +589,28 @@ interface FlowConfigAPI {
574
589
  title: string;
575
590
  description?: string;
576
591
  }
577
- type FlowFunction<C extends FlowConfig, I extends any = {}> = (ctx: FlowContext<C>, inputs: I) => Promise<void>;
592
+ type FlowFunction<C extends FlowConfig, E extends any = {}, S extends any = {}, I extends any = {}> = (ctx: FlowContext<C, E, S>, inputs: I) => Promise<void>;
578
593
  type ExtractStageKeys<T extends FlowConfig> = T extends {
579
594
  stages: infer S;
580
595
  } ? S extends ReadonlyArray<infer U> ? U extends string ? U : U extends {
581
596
  key: infer K extends string;
582
597
  } ? K : never : never : never;
583
598
  type StageConfigObject = {
599
+ /** The unique key of the stage. */
584
600
  key: string;
601
+ /** The name of the stage as shown in the Console. */
585
602
  name: string;
603
+ /** The description of the stage as shown in the Console. */
586
604
  description?: string;
605
+ /** Whether the stage is initially hidden in the Console. */
587
606
  initiallyHidden?: boolean;
588
607
  };
589
608
  type StageConfig = string | StageConfigObject;
590
- declare function createFlowContext<C extends FlowConfig>(runId: string, data: any, spanId: string): FlowContext<C>;
609
+ declare function createFlowContext<C extends FlowConfig, E = any, S = any>(runId: string, data: any, action: string | null, spanId: string, ctx: {
610
+ env: E;
611
+ now: Date;
612
+ secrets: S;
613
+ }): FlowContext<C, E, S>;
591
614
 
592
615
  declare function ksuid(): string;
593
616
 
package/dist/index.d.ts CHANGED
@@ -545,19 +545,31 @@ type UIApiResponses = {
545
545
  };
546
546
  };
547
547
 
548
- interface FlowContext<C extends FlowConfig> {
548
+ interface FlowContext<C extends FlowConfig, E = any, S = any> {
549
549
  step: Step<C>;
550
550
  ui: UI<C>;
551
+ env: E;
552
+ now: Date;
553
+ secrets: S;
551
554
  }
552
555
  type JsonSerializable = string | number | boolean | null | JsonSerializable[] | {
553
556
  [key: string]: JsonSerializable;
554
557
  };
555
558
  type Step<C extends FlowConfig> = {
556
- <R extends JsonSerializable | void>(name: string, options: {
559
+ <R extends JsonSerializable | void>(
560
+ /** The unique name of this step. */
561
+ name: string,
562
+ /** Configuration options for the step. */
563
+ options: {
564
+ /** The stage this step belongs to. Used for organising steps in the UI. */
557
565
  stage?: ExtractStageKeys<C>;
558
- maxRetries?: number;
559
- timeoutInMs?: number;
560
- }, fn: () => Promise<R> & {
566
+ /** Number of times to retry the step if it fails. Defaults to 5. */
567
+ retries?: number;
568
+ /** Maximum time in milliseconds to wait for the step to complete. Defaults to 60000 (1 minute). */
569
+ timeout?: number;
570
+ },
571
+ /** The step function to execute. */
572
+ fn: () => Promise<R> & {
561
573
  catch: (errorHandler: (err: Error) => Promise<void> | void) => Promise<any>;
562
574
  }): Promise<R>;
563
575
  <R extends JsonSerializable | void>(name: string, fn: () => Promise<R> & {
@@ -565,8 +577,11 @@ type Step<C extends FlowConfig> = {
565
577
  }): Promise<R>;
566
578
  };
567
579
  interface FlowConfig {
580
+ /** The stages to organise the steps in the flow. */
568
581
  stages?: StageConfig[];
582
+ /** The title of the flow as shown in the Console. */
569
583
  title?: string;
584
+ /** The description of the flow as shown in the Console. */
570
585
  description?: string;
571
586
  }
572
587
  interface FlowConfigAPI {
@@ -574,20 +589,28 @@ interface FlowConfigAPI {
574
589
  title: string;
575
590
  description?: string;
576
591
  }
577
- type FlowFunction<C extends FlowConfig, I extends any = {}> = (ctx: FlowContext<C>, inputs: I) => Promise<void>;
592
+ type FlowFunction<C extends FlowConfig, E extends any = {}, S extends any = {}, I extends any = {}> = (ctx: FlowContext<C, E, S>, inputs: I) => Promise<void>;
578
593
  type ExtractStageKeys<T extends FlowConfig> = T extends {
579
594
  stages: infer S;
580
595
  } ? S extends ReadonlyArray<infer U> ? U extends string ? U : U extends {
581
596
  key: infer K extends string;
582
597
  } ? K : never : never : never;
583
598
  type StageConfigObject = {
599
+ /** The unique key of the stage. */
584
600
  key: string;
601
+ /** The name of the stage as shown in the Console. */
585
602
  name: string;
603
+ /** The description of the stage as shown in the Console. */
586
604
  description?: string;
605
+ /** Whether the stage is initially hidden in the Console. */
587
606
  initiallyHidden?: boolean;
588
607
  };
589
608
  type StageConfig = string | StageConfigObject;
590
- declare function createFlowContext<C extends FlowConfig>(runId: string, data: any, spanId: string): FlowContext<C>;
609
+ declare function createFlowContext<C extends FlowConfig, E = any, S = any>(runId: string, data: any, action: string | null, spanId: string, ctx: {
610
+ env: E;
611
+ now: Date;
612
+ secrets: S;
613
+ }): FlowContext<C, E, S>;
591
614
 
592
615
  declare function ksuid(): string;
593
616
 
package/dist/index.js CHANGED
@@ -11,10 +11,10 @@ import { sql as sql3 } from "kysely";
11
11
  // src/database.ts
12
12
  import { Kysely, PostgresDialect } from "kysely";
13
13
  import * as neon from "@neondatabase/serverless";
14
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
14
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
15
15
 
16
16
  // src/auditing.js
17
- import { AsyncLocalStorage } from "node:async_hooks";
17
+ import { AsyncLocalStorage } from "async_hooks";
18
18
  import TraceParent from "traceparent";
19
19
  import { sql, SelectionNode } from "kysely";
20
20
  var auditContextStorage = new AsyncLocalStorage();
@@ -354,7 +354,7 @@ __name(spanNameForModelAPI, "spanNameForModelAPI");
354
354
 
355
355
  // src/database.ts
356
356
  import WebSocket from "ws";
357
- import { readFileSync } from "node:fs";
357
+ import { readFileSync } from "fs";
358
358
  var dbInstance = new AsyncLocalStorage2();
359
359
  var vitestDb = null;
360
360
  async function withDatabase(db, requiresTransaction, cb) {
@@ -2430,9 +2430,16 @@ var selectOne = /* @__PURE__ */ __name((name, options) => {
2430
2430
  }, "selectOne");
2431
2431
 
2432
2432
  // src/flows/ui/page.ts
2433
- async function page(options, data) {
2433
+ async function page(options, data, action) {
2434
2434
  const content = options.content;
2435
2435
  let hasValidationErrors = false;
2436
+ if (options.actions) {
2437
+ const isValidAction = options.actions.some((a) => {
2438
+ if (typeof a === "string") return a === action;
2439
+ return a && typeof a === "object" && "label" in a && a.label === action;
2440
+ });
2441
+ hasValidationErrors = !isValidAction;
2442
+ }
2436
2443
  const contentUiConfig = await Promise.all(
2437
2444
  content.map(async (c) => {
2438
2445
  const isInput = "__type" in c && c.__type == "input";
@@ -2583,15 +2590,33 @@ var header = /* @__PURE__ */ __name((options) => {
2583
2590
 
2584
2591
  // src/flows/index.ts
2585
2592
  var defaultOpts = {
2586
- maxRetries: 5,
2587
- timeoutInMs: 6e4
2593
+ retries: 5,
2594
+ timeout: 6e4
2588
2595
  };
2589
- function createFlowContext(runId, data, spanId) {
2596
+ function createFlowContext(runId, data, action, spanId, ctx) {
2597
+ const usedNames = /* @__PURE__ */ new Set();
2590
2598
  return {
2599
+ env: ctx.env,
2600
+ now: ctx.now,
2601
+ secrets: ctx.secrets,
2591
2602
  step: /* @__PURE__ */ __name(async (name, optionsOrFn, fn) => {
2592
2603
  const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
2593
2604
  const actualFn = typeof optionsOrFn === "function" ? optionsOrFn : fn;
2594
2605
  const db = useDatabase();
2606
+ if (usedNames.has(name)) {
2607
+ await db.insertInto("keel.flow_step").values({
2608
+ run_id: runId,
2609
+ name,
2610
+ stage: options.stage,
2611
+ status: "FAILED" /* FAILED */,
2612
+ type: "FUNCTION" /* FUNCTION */,
2613
+ error: `Duplicate step name: ${name}`,
2614
+ startTime: /* @__PURE__ */ new Date(),
2615
+ endTime: /* @__PURE__ */ new Date()
2616
+ }).returningAll().executeTakeFirst();
2617
+ throw new Error(`Duplicate step name: ${name}`);
2618
+ }
2619
+ usedNames.add(name);
2595
2620
  const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().execute();
2596
2621
  const newSteps = past.filter((step) => step.status === "NEW" /* NEW */);
2597
2622
  const completedSteps = past.filter(
@@ -2622,7 +2647,7 @@ function createFlowContext(runId, data, spanId) {
2622
2647
  try {
2623
2648
  result = await withTimeout(
2624
2649
  actualFn(),
2625
- options.timeoutInMs ?? defaultOpts.timeoutInMs
2650
+ options.timeout ?? defaultOpts.timeout
2626
2651
  );
2627
2652
  } catch (e) {
2628
2653
  await db.updateTable("keel.flow_step").set({
@@ -2631,7 +2656,7 @@ function createFlowContext(runId, data, spanId) {
2631
2656
  endTime: /* @__PURE__ */ new Date(),
2632
2657
  error: e instanceof Error ? e.message : "An error occurred"
2633
2658
  }).where("id", "=", newSteps[0].id).returningAll().executeTakeFirst();
2634
- if (failedSteps.length + 1 >= (options.maxRetries ?? defaultOpts.maxRetries)) {
2659
+ if (failedSteps.length + 1 >= (options.retries ?? defaultOpts.retries)) {
2635
2660
  throw new ExhuastedRetriesDisrupt();
2636
2661
  }
2637
2662
  await db.insertInto("keel.flow_step").values({
@@ -2663,8 +2688,25 @@ function createFlowContext(runId, data, spanId) {
2663
2688
  ui: {
2664
2689
  page: /* @__PURE__ */ __name(async (name, options) => {
2665
2690
  const db = useDatabase();
2691
+ if (usedNames.has(name)) {
2692
+ await db.insertInto("keel.flow_step").values({
2693
+ run_id: runId,
2694
+ name,
2695
+ stage: options.stage,
2696
+ status: "FAILED" /* FAILED */,
2697
+ type: "UI" /* UI */,
2698
+ error: `Duplicate step name: ${name}`,
2699
+ startTime: /* @__PURE__ */ new Date(),
2700
+ endTime: /* @__PURE__ */ new Date()
2701
+ }).returningAll().executeTakeFirst();
2702
+ throw new Error(`Duplicate step name: ${name}`);
2703
+ }
2704
+ usedNames.add(name);
2666
2705
  let step = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().executeTakeFirst();
2667
2706
  if (step && step.status === "COMPLETED" /* COMPLETED */) {
2707
+ if (step.action) {
2708
+ return { data: step.value, action: step.action };
2709
+ }
2668
2710
  return step.value;
2669
2711
  }
2670
2712
  if (!step) {
@@ -2676,22 +2718,29 @@ function createFlowContext(runId, data, spanId) {
2676
2718
  type: "UI" /* UI */,
2677
2719
  startTime: /* @__PURE__ */ new Date()
2678
2720
  }).returningAll().executeTakeFirst();
2679
- throw new UIRenderDisrupt(step?.id, (await page(options, null)).page);
2721
+ throw new UIRenderDisrupt(
2722
+ step?.id,
2723
+ (await page(options, null, null)).page
2724
+ );
2680
2725
  }
2681
2726
  if (!data) {
2682
- throw new UIRenderDisrupt(step?.id, (await page(options, null)).page);
2727
+ throw new UIRenderDisrupt(
2728
+ step?.id,
2729
+ (await page(options, null, null)).page
2730
+ );
2683
2731
  }
2684
- const p = await page(options, data);
2732
+ const p = await page(options, data, action);
2685
2733
  if (p.hasValidationErrors) {
2686
2734
  throw new UIRenderDisrupt(step?.id, p.page);
2687
2735
  }
2688
2736
  await db.updateTable("keel.flow_step").set({
2689
2737
  status: "COMPLETED" /* COMPLETED */,
2690
2738
  value: JSON.stringify(data),
2739
+ action,
2691
2740
  spanId,
2692
2741
  endTime: /* @__PURE__ */ new Date()
2693
2742
  }).where("id", "=", step.id).returningAll().executeTakeFirst();
2694
- return data;
2743
+ return { data, action };
2695
2744
  }, "page"),
2696
2745
  inputs: {
2697
2746
  text: textInput,
@@ -2746,7 +2795,7 @@ async function handleFlow(request, config) {
2746
2795
  if (!runId) {
2747
2796
  throw new Error("no runId provided");
2748
2797
  }
2749
- const { flows } = config;
2798
+ const { flows, createFlowContextAPI } = config;
2750
2799
  if (!(request.method in flows)) {
2751
2800
  const message = `no corresponding flow found for '${request.method}'`;
2752
2801
  span.setStatus({
@@ -2762,14 +2811,14 @@ async function handleFlow(request, config) {
2762
2811
  db = createDatabaseClient({
2763
2812
  connString: request.meta?.secrets?.KEEL_DB_CONN
2764
2813
  });
2765
- const flowRun = await db.selectFrom("keel.flow_run").where("id", "=", runId).selectAll().executeTakeFirst();
2766
- if (!flowRun) {
2767
- throw new Error("no flow run found");
2768
- }
2769
2814
  const ctx = createFlowContext(
2770
2815
  request.meta.runId,
2771
2816
  request.meta.data,
2772
- span.spanContext().spanId
2817
+ request.meta.action,
2818
+ span.spanContext().spanId,
2819
+ createFlowContextAPI({
2820
+ meta: request.meta
2821
+ })
2773
2822
  );
2774
2823
  const flowFunction = flows[request.method].fn;
2775
2824
  const rawFlowConfig = flows[request.method].config;
@@ -2786,7 +2835,7 @@ async function handleFlow(request, config) {
2786
2835
  return stage;
2787
2836
  })
2788
2837
  };
2789
- const inputs = parseInputs(flowRun.input);
2838
+ const inputs = parseInputs(request.meta?.inputs);
2790
2839
  try {
2791
2840
  await tryExecuteFlow(db, async () => {
2792
2841
  return flowFunction(ctx, inputs);