@teamkeel/functions-runtime 0.460.0 → 0.461.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.js CHANGED
@@ -472,6 +472,20 @@ function isRichType(obj) {
472
472
  }
473
473
  __name(isRichType, "isRichType");
474
474
 
475
+ // src/jsonColumnValue.js
476
+ var JsonColumnValue = class {
477
+ static {
478
+ __name(this, "JsonColumnValue");
479
+ }
480
+ constructor(value) {
481
+ this.value = value;
482
+ }
483
+ };
484
+ function parseJsonColumnValue(value) {
485
+ return new JsonColumnValue(JSON.parse(value));
486
+ }
487
+ __name(parseJsonColumnValue, "parseJsonColumnValue");
488
+
475
489
  // src/camelCasePlugin.js
476
490
  var KeelCamelCasePlugin = class {
477
491
  static {
@@ -479,41 +493,77 @@ var KeelCamelCasePlugin = class {
479
493
  }
480
494
  constructor(opt) {
481
495
  this.opt = opt;
496
+ this.auditQueryIds = /* @__PURE__ */ new WeakSet();
482
497
  this.CamelCasePlugin = new CamelCasePlugin({
483
498
  ...opt,
484
499
  underscoreBeforeDigits: true
485
500
  });
486
501
  }
487
502
  transformQuery(args) {
503
+ if (args.queryId && referencesTable(args.node, "keel_audit")) {
504
+ this.auditQueryIds.add(args.queryId);
505
+ }
488
506
  return this.CamelCasePlugin.transformQuery(args);
489
507
  }
490
508
  async transformResult(args) {
491
509
  if (args.result.rows && Array.isArray(args.result.rows)) {
510
+ const mapAuditJson = args.queryId && this.auditQueryIds.has(args.queryId);
492
511
  return {
493
512
  ...args.result,
494
- rows: args.result.rows.map((row) => this.mapRow(row))
513
+ rows: args.result.rows.map((row) => this.mapRow(row, { mapAuditJson }))
495
514
  };
496
515
  }
497
516
  return args.result;
498
517
  }
499
- mapRow(row) {
518
+ mapRow(row, context7 = {}) {
500
519
  return Object.keys(row).reduce((obj, key) => {
501
520
  if (key.endsWith("__sequence")) {
502
521
  return obj;
503
522
  }
504
523
  let value = row[key];
524
+ if (value instanceof JsonColumnValue) {
525
+ value = shouldMapJsonColumn(row, key, context7) && canMap(value.value, this.opt) ? this.mapRow(value.value, context7) : value.value;
526
+ obj[this.CamelCasePlugin.camelCase(key)] = value;
527
+ return obj;
528
+ }
505
529
  if (Array.isArray(value)) {
506
530
  value = value.map(
507
- (it) => canMap(it, this.opt) ? this.mapRow(it) : it
531
+ (it) => canMap(it, this.opt) ? this.mapRow(it, context7) : it
508
532
  );
509
533
  } else if (canMap(value, this.opt)) {
510
- value = this.mapRow(value);
534
+ value = this.mapRow(value, context7);
511
535
  }
512
536
  obj[this.CamelCasePlugin.camelCase(key)] = value;
513
537
  return obj;
514
538
  }, {});
515
539
  }
516
540
  };
541
+ function shouldMapJsonColumn(row, key, context7) {
542
+ return context7.mapAuditJson && key === "data" && "id" in row && "table_name" in row && "op" in row && "identity_id" in row && "trace_id" in row && "created_at" in row && "event_processed_at" in row;
543
+ }
544
+ __name(shouldMapJsonColumn, "shouldMapJsonColumn");
545
+ function referencesTable(node, tableName) {
546
+ if (!node || typeof node !== "object") {
547
+ return false;
548
+ }
549
+ if (node.kind === "IdentifierNode" && (node.name === tableName || node.name === "keelAudit")) {
550
+ return true;
551
+ }
552
+ if (node.kind === "RawNode" && Array.isArray(node.sqlFragments) && node.sqlFragments.some(
553
+ (fragment) => new RegExp(`(^|[^A-Za-z0-9_])${tableName}($|[^A-Za-z0-9_])`, "i").test(
554
+ fragment
555
+ )
556
+ )) {
557
+ return true;
558
+ }
559
+ return Object.values(node).some((value) => {
560
+ if (Array.isArray(value)) {
561
+ return value.some((item) => referencesTable(item, tableName));
562
+ }
563
+ return referencesTable(value, tableName);
564
+ });
565
+ }
566
+ __name(referencesTable, "referencesTable");
517
567
  function canMap(obj, opt) {
518
568
  return isPlainObject(obj) && !opt?.maintainNestedObjectKeys && !isRichType(obj);
519
569
  }
@@ -914,6 +964,8 @@ function getDialect(connString) {
914
964
  pgTypes.builtins.INTERVAL,
915
965
  (val) => new Duration(val)
916
966
  );
967
+ pgTypes.setTypeParser(pgTypes.builtins.JSON, parseJsonColumnValue);
968
+ pgTypes.setTypeParser(pgTypes.builtins.JSONB, parseJsonColumnValue);
917
969
  const poolConfig = {
918
970
  Client: InstrumentedClient,
919
971
  // Increased idle time before closing a connection in the local pool (from 10s default).
@@ -948,6 +1000,8 @@ function getDialect(connString) {
948
1000
  pgTypes.builtins.INTERVAL,
949
1001
  (val) => new Duration(val)
950
1002
  );
1003
+ neon.types.setTypeParser(pgTypes.builtins.JSON, parseJsonColumnValue);
1004
+ neon.types.setTypeParser(pgTypes.builtins.JSONB, parseJsonColumnValue);
951
1005
  neon.neonConfig.webSocketConstructor = WebSocket;
952
1006
  const pool = new InstrumentedNeonServerlessPool({
953
1007
  // If connString is not passed fall back to reading from env var
@@ -2900,30 +2954,30 @@ var FlowsAPI = class _FlowsAPI {
2900
2954
  }
2901
2955
  };
2902
2956
 
2903
- // src/integrationServer.js
2957
+ // src/auth/serviceToken.ts
2904
2958
  import jwt3 from "jsonwebtoken";
2905
- function buildHeaders3(identity) {
2906
- const headers = { "Content-Type": "application/json" };
2959
+ function buildServiceAuthHeaders(audience) {
2907
2960
  const base64pk = process.env.KEEL_PRIVATE_KEY;
2908
2961
  if (!base64pk) {
2909
2962
  throw new Error(
2910
- "KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
2963
+ `KEEL_PRIVATE_KEY is not set; cannot sign the ${audience} service token`
2911
2964
  );
2912
2965
  }
2913
2966
  const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
2914
- const subject = identity && identity.id ? identity.id : "integration-proxy";
2915
- headers["Authorization"] = "Bearer " + jwt3.sign({}, privateKey, {
2967
+ const token = jwt3.sign({}, privateKey, {
2916
2968
  algorithm: "RS256",
2917
2969
  expiresIn: 60 * 60 * 24,
2918
- subject,
2919
- // Scope the token to the integration proxy so it can't be mistaken for an ordinary user
2920
- // access token; the runtime proxy verifies this audience before injecting credentials.
2921
- audience: "integration-proxy",
2970
+ audience,
2922
2971
  issuer: "https://keel.so"
2923
2972
  });
2924
- return headers;
2973
+ return {
2974
+ "Content-Type": "application/json",
2975
+ Authorization: `Bearer ${token}`
2976
+ };
2925
2977
  }
2926
- __name(buildHeaders3, "buildHeaders");
2978
+ __name(buildServiceAuthHeaders, "buildServiceAuthHeaders");
2979
+
2980
+ // src/integrationServer.js
2927
2981
  function getApiUrl3() {
2928
2982
  const apiUrl = process.env.KEEL_API_URL;
2929
2983
  if (!apiUrl) {
@@ -2940,7 +2994,7 @@ function createIntegrationServer(name, identity) {
2940
2994
  )}/proxy`;
2941
2995
  const response = await fetch(url, {
2942
2996
  method: "POST",
2943
- headers: buildHeaders3(identity ?? null),
2997
+ headers: buildServiceAuthHeaders("integration-proxy"),
2944
2998
  body: JSON.stringify(request ?? {})
2945
2999
  });
2946
3000
  if (!response.ok) {
@@ -3723,12 +3777,29 @@ async function applyElementGetData(content, data) {
3723
3777
  return data;
3724
3778
  }
3725
3779
  __name(applyElementGetData, "applyElementGetData");
3726
- async function callbackFn(elements, elementName, callbackName, data) {
3727
- const element = elements.find(
3728
- (el) => el.uiConfig && el.uiConfig.name === elementName
3780
+ var ITERATOR_SCOPED_ELEMENT = /^([^[]+)\[(\d+)\]:(.+)$/;
3781
+ async function callbackFn(elements, elementPath, callbackName, data) {
3782
+ const scoped = ITERATOR_SCOPED_ELEMENT.exec(elementPath);
3783
+ let searchScope = elements;
3784
+ let elementName = elementPath;
3785
+ let iteratorName = null;
3786
+ if (scoped) {
3787
+ iteratorName = scoped[1];
3788
+ elementName = scoped[3];
3789
+ const iter = elements.find(
3790
+ (el) => el?.uiConfig?.__type === "ui.iterator" && el.uiConfig.name === iteratorName
3791
+ );
3792
+ if (!iter) {
3793
+ throw new Error(`Iterator with name ${iteratorName} not found`);
3794
+ }
3795
+ searchScope = iter.uiConfig.content;
3796
+ }
3797
+ const element = searchScope.find(
3798
+ (el) => el?.uiConfig && el.uiConfig.name === elementName
3729
3799
  );
3730
3800
  if (!element) {
3731
- throw new Error(`Element with name ${elementName} not found`);
3801
+ const where = iteratorName ? ` in iterator ${iteratorName}` : "";
3802
+ throw new Error(`Element with name ${elementName} not found${where}`);
3732
3803
  }
3733
3804
  const cb = element[callbackName];
3734
3805
  if (typeof cb !== "function") {
@@ -4674,6 +4745,19 @@ var defaultOpts = {
4674
4745
  retries: 4,
4675
4746
  timeout: 6e4
4676
4747
  };
4748
+ var STEP_VALUE_WARN_BYTES = 1024 * 1024;
4749
+ function serializeStepValue(span, name, value) {
4750
+ const serialized = JSON.stringify(value);
4751
+ const bytes = serialized == null ? 0 : Buffer.byteLength(serialized, "utf8");
4752
+ span.setAttribute("step.value.bytes", bytes);
4753
+ if (bytes > STEP_VALUE_WARN_BYTES) {
4754
+ console.warn(
4755
+ `flow step "${name}" returned a ${Math.round(bytes / 1024)}KB value. Step values are reloaded on every flow invocation during replay, so large values slow the whole run down. Consider storing large data as a file and returning a reference to it instead.`
4756
+ );
4757
+ }
4758
+ return serialized;
4759
+ }
4760
+ __name(serializeStepValue, "serializeStepValue");
4677
4761
  async function insertNewStep(db, runId, name, stage) {
4678
4762
  await db.transaction().execute(async (trx) => {
4679
4763
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
@@ -4694,6 +4778,30 @@ async function insertNewStep(db, runId, name, stage) {
4694
4778
  __name(insertNewStep, "insertNewStep");
4695
4779
  function createFlowContext(runId, data, action, callback, element, spanId, ctx) {
4696
4780
  const usedNames = /* @__PURE__ */ new Set();
4781
+ let stepsSnapshot = null;
4782
+ let uiBoundaryCrossed = false;
4783
+ async function loadStepsSnapshot(db) {
4784
+ if (stepsSnapshot) {
4785
+ return stepsSnapshot;
4786
+ }
4787
+ const rows = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).select(["id", "name", "status", "type", "action"]).select(sql4`value::text`.as("valueRaw")).execute();
4788
+ const map = /* @__PURE__ */ new Map();
4789
+ for (const row of rows) {
4790
+ const existing = map.get(row.name);
4791
+ if (existing) {
4792
+ existing.push(row);
4793
+ } else {
4794
+ map.set(row.name, [row]);
4795
+ }
4796
+ }
4797
+ stepsSnapshot = map;
4798
+ return map;
4799
+ }
4800
+ __name(loadStepsSnapshot, "loadStepsSnapshot");
4801
+ async function readStepRows(db, name) {
4802
+ return await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).select(["id", "name", "status", "type", "action"]).select(sql4`value::text`.as("valueRaw")).execute();
4803
+ }
4804
+ __name(readStepRows, "readStepRows");
4697
4805
  const context7 = {
4698
4806
  identity: ctx.identity,
4699
4807
  env: ctx.env,
@@ -4733,7 +4841,15 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4733
4841
  throw new Error(`Duplicate step name: ${name}`);
4734
4842
  }
4735
4843
  usedNames.add(name);
4736
- const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(sql4`value::text`.as("valueRaw")).execute();
4844
+ let past;
4845
+ if (uiBoundaryCrossed) {
4846
+ past = await readStepRows(db, name);
4847
+ } else {
4848
+ past = (await loadStepsSnapshot(db)).get(name) ?? [];
4849
+ if (!past.some((step) => step.status === "COMPLETED" /* COMPLETED */)) {
4850
+ past = await readStepRows(db, name);
4851
+ }
4852
+ }
4737
4853
  const newSteps = past.filter(
4738
4854
  (step) => step.status === "NEW" /* NEW */
4739
4855
  );
@@ -4816,7 +4932,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4816
4932
  }
4817
4933
  await db.updateTable("keel.flow_step").set({
4818
4934
  status: "COMPLETED" /* COMPLETED */,
4819
- value: JSON.stringify(result),
4935
+ value: serializeStepValue(span, name, result),
4820
4936
  spanId,
4821
4937
  endTime: /* @__PURE__ */ new Date()
4822
4938
  }).where("id", "=", newSteps[0].id).returningAll().executeTakeFirst();
@@ -4854,6 +4970,20 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4854
4970
  throw new Error(`Duplicate step name: ${name}`);
4855
4971
  }
4856
4972
  usedNames.add(name);
4973
+ const replayCompletedUiStep = /* @__PURE__ */ __name(async (completed) => {
4974
+ span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4975
+ span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4976
+ const rawValue = completed.valueRaw;
4977
+ const storedData = rawValue == null ? null : JSON.parse(rawValue);
4978
+ const parsedData2 = await applyElementGetData(
4979
+ options.content,
4980
+ transformRichDataTypes(storedData)
4981
+ );
4982
+ if (completed.action) {
4983
+ return { data: parsedData2, action: completed.action };
4984
+ }
4985
+ return parsedData2;
4986
+ }, "replayCompletedUiStep");
4857
4987
  const { step, inserted } = await db.transaction().execute(async (trx) => {
4858
4988
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
4859
4989
  const existing = await trx.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(sql4`value::text`.as("valueRaw")).executeTakeFirst();
@@ -4872,18 +5002,8 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4872
5002
  return { step: created, inserted: true };
4873
5003
  });
4874
5004
  if (step && step.status === "COMPLETED" /* COMPLETED */) {
4875
- span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4876
- span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4877
- const rawValue = step.valueRaw;
4878
- const storedData = rawValue == null ? null : JSON.parse(rawValue);
4879
- const parsedData2 = await applyElementGetData(
4880
- options.content,
4881
- transformRichDataTypes(storedData)
4882
- );
4883
- if (step.action) {
4884
- return { data: parsedData2, action: step.action };
4885
- }
4886
- return parsedData2;
5005
+ uiBoundaryCrossed = true;
5006
+ return replayCompletedUiStep(step);
4887
5007
  }
4888
5008
  if (inserted) {
4889
5009
  span.setAttribute("rendered", true);
@@ -4942,12 +5062,13 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4942
5062
  }
4943
5063
  await db.updateTable("keel.flow_step").set({
4944
5064
  status: "COMPLETED" /* COMPLETED */,
4945
- value: JSON.stringify(data),
5065
+ value: serializeStepValue(span, name, data),
4946
5066
  action,
4947
5067
  spanId,
4948
5068
  endTime: /* @__PURE__ */ new Date()
4949
5069
  }).where("id", "=", step.id).returningAll().executeTakeFirst();
4950
5070
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
5071
+ uiBoundaryCrossed = true;
4951
5072
  const parsedData = await applyElementGetData(
4952
5073
  options.content,
4953
5074
  transformRichDataTypes(data)
@@ -5361,7 +5482,7 @@ async function notifyEmail(input) {
5361
5482
  };
5362
5483
  const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
5363
5484
  method: "POST",
5364
- headers: { "Content-Type": "application/json" },
5485
+ headers: buildServiceAuthHeaders("notifications"),
5365
5486
  body: JSON.stringify(body)
5366
5487
  });
5367
5488
  if (!response.ok) {