@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.cjs CHANGED
@@ -530,6 +530,20 @@ function isRichType(obj) {
530
530
  }
531
531
  __name(isRichType, "isRichType");
532
532
 
533
+ // src/jsonColumnValue.js
534
+ var JsonColumnValue = class {
535
+ static {
536
+ __name(this, "JsonColumnValue");
537
+ }
538
+ constructor(value) {
539
+ this.value = value;
540
+ }
541
+ };
542
+ function parseJsonColumnValue(value) {
543
+ return new JsonColumnValue(JSON.parse(value));
544
+ }
545
+ __name(parseJsonColumnValue, "parseJsonColumnValue");
546
+
533
547
  // src/camelCasePlugin.js
534
548
  var KeelCamelCasePlugin = class {
535
549
  static {
@@ -537,41 +551,77 @@ var KeelCamelCasePlugin = class {
537
551
  }
538
552
  constructor(opt) {
539
553
  this.opt = opt;
554
+ this.auditQueryIds = /* @__PURE__ */ new WeakSet();
540
555
  this.CamelCasePlugin = new import_kysely2.CamelCasePlugin({
541
556
  ...opt,
542
557
  underscoreBeforeDigits: true
543
558
  });
544
559
  }
545
560
  transformQuery(args) {
561
+ if (args.queryId && referencesTable(args.node, "keel_audit")) {
562
+ this.auditQueryIds.add(args.queryId);
563
+ }
546
564
  return this.CamelCasePlugin.transformQuery(args);
547
565
  }
548
566
  async transformResult(args) {
549
567
  if (args.result.rows && Array.isArray(args.result.rows)) {
568
+ const mapAuditJson = args.queryId && this.auditQueryIds.has(args.queryId);
550
569
  return {
551
570
  ...args.result,
552
- rows: args.result.rows.map((row) => this.mapRow(row))
571
+ rows: args.result.rows.map((row) => this.mapRow(row, { mapAuditJson }))
553
572
  };
554
573
  }
555
574
  return args.result;
556
575
  }
557
- mapRow(row) {
576
+ mapRow(row, context7 = {}) {
558
577
  return Object.keys(row).reduce((obj, key) => {
559
578
  if (key.endsWith("__sequence")) {
560
579
  return obj;
561
580
  }
562
581
  let value = row[key];
582
+ if (value instanceof JsonColumnValue) {
583
+ value = shouldMapJsonColumn(row, key, context7) && canMap(value.value, this.opt) ? this.mapRow(value.value, context7) : value.value;
584
+ obj[this.CamelCasePlugin.camelCase(key)] = value;
585
+ return obj;
586
+ }
563
587
  if (Array.isArray(value)) {
564
588
  value = value.map(
565
- (it) => canMap(it, this.opt) ? this.mapRow(it) : it
589
+ (it) => canMap(it, this.opt) ? this.mapRow(it, context7) : it
566
590
  );
567
591
  } else if (canMap(value, this.opt)) {
568
- value = this.mapRow(value);
592
+ value = this.mapRow(value, context7);
569
593
  }
570
594
  obj[this.CamelCasePlugin.camelCase(key)] = value;
571
595
  return obj;
572
596
  }, {});
573
597
  }
574
598
  };
599
+ function shouldMapJsonColumn(row, key, context7) {
600
+ 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;
601
+ }
602
+ __name(shouldMapJsonColumn, "shouldMapJsonColumn");
603
+ function referencesTable(node, tableName) {
604
+ if (!node || typeof node !== "object") {
605
+ return false;
606
+ }
607
+ if (node.kind === "IdentifierNode" && (node.name === tableName || node.name === "keelAudit")) {
608
+ return true;
609
+ }
610
+ if (node.kind === "RawNode" && Array.isArray(node.sqlFragments) && node.sqlFragments.some(
611
+ (fragment) => new RegExp(`(^|[^A-Za-z0-9_])${tableName}($|[^A-Za-z0-9_])`, "i").test(
612
+ fragment
613
+ )
614
+ )) {
615
+ return true;
616
+ }
617
+ return Object.values(node).some((value) => {
618
+ if (Array.isArray(value)) {
619
+ return value.some((item) => referencesTable(item, tableName));
620
+ }
621
+ return referencesTable(value, tableName);
622
+ });
623
+ }
624
+ __name(referencesTable, "referencesTable");
575
625
  function canMap(obj, opt) {
576
626
  return isPlainObject(obj) && !opt?.maintainNestedObjectKeys && !isRichType(obj);
577
627
  }
@@ -972,6 +1022,8 @@ function getDialect(connString) {
972
1022
  import_pg.types.builtins.INTERVAL,
973
1023
  (val) => new Duration(val)
974
1024
  );
1025
+ import_pg.types.setTypeParser(import_pg.types.builtins.JSON, parseJsonColumnValue);
1026
+ import_pg.types.setTypeParser(import_pg.types.builtins.JSONB, parseJsonColumnValue);
975
1027
  const poolConfig = {
976
1028
  Client: InstrumentedClient,
977
1029
  // Increased idle time before closing a connection in the local pool (from 10s default).
@@ -1006,6 +1058,8 @@ function getDialect(connString) {
1006
1058
  import_pg.types.builtins.INTERVAL,
1007
1059
  (val) => new Duration(val)
1008
1060
  );
1061
+ neon.types.setTypeParser(import_pg.types.builtins.JSON, parseJsonColumnValue);
1062
+ neon.types.setTypeParser(import_pg.types.builtins.JSONB, parseJsonColumnValue);
1009
1063
  neon.neonConfig.webSocketConstructor = import_ws.default;
1010
1064
  const pool = new InstrumentedNeonServerlessPool({
1011
1065
  // If connString is not passed fall back to reading from env var
@@ -2955,30 +3009,30 @@ var FlowsAPI = class _FlowsAPI {
2955
3009
  }
2956
3010
  };
2957
3011
 
2958
- // src/integrationServer.js
3012
+ // src/auth/serviceToken.ts
2959
3013
  var import_jsonwebtoken3 = __toESM(require("jsonwebtoken"), 1);
2960
- function buildHeaders3(identity) {
2961
- const headers = { "Content-Type": "application/json" };
3014
+ function buildServiceAuthHeaders(audience) {
2962
3015
  const base64pk = process.env.KEEL_PRIVATE_KEY;
2963
3016
  if (!base64pk) {
2964
3017
  throw new Error(
2965
- "KEEL_PRIVATE_KEY is not set; cannot sign the integration proxy token"
3018
+ `KEEL_PRIVATE_KEY is not set; cannot sign the ${audience} service token`
2966
3019
  );
2967
3020
  }
2968
3021
  const privateKey = Buffer.from(base64pk, "base64").toString("utf8");
2969
- const subject = identity && identity.id ? identity.id : "integration-proxy";
2970
- headers["Authorization"] = "Bearer " + import_jsonwebtoken3.default.sign({}, privateKey, {
3022
+ const token = import_jsonwebtoken3.default.sign({}, privateKey, {
2971
3023
  algorithm: "RS256",
2972
3024
  expiresIn: 60 * 60 * 24,
2973
- subject,
2974
- // Scope the token to the integration proxy so it can't be mistaken for an ordinary user
2975
- // access token; the runtime proxy verifies this audience before injecting credentials.
2976
- audience: "integration-proxy",
3025
+ audience,
2977
3026
  issuer: "https://keel.so"
2978
3027
  });
2979
- return headers;
3028
+ return {
3029
+ "Content-Type": "application/json",
3030
+ Authorization: `Bearer ${token}`
3031
+ };
2980
3032
  }
2981
- __name(buildHeaders3, "buildHeaders");
3033
+ __name(buildServiceAuthHeaders, "buildServiceAuthHeaders");
3034
+
3035
+ // src/integrationServer.js
2982
3036
  function getApiUrl3() {
2983
3037
  const apiUrl = process.env.KEEL_API_URL;
2984
3038
  if (!apiUrl) {
@@ -2995,7 +3049,7 @@ function createIntegrationServer(name, identity) {
2995
3049
  )}/proxy`;
2996
3050
  const response = await fetch(url, {
2997
3051
  method: "POST",
2998
- headers: buildHeaders3(identity ?? null),
3052
+ headers: buildServiceAuthHeaders("integration-proxy"),
2999
3053
  body: JSON.stringify(request ?? {})
3000
3054
  });
3001
3055
  if (!response.ok) {
@@ -3758,12 +3812,29 @@ async function applyElementGetData(content, data) {
3758
3812
  return data;
3759
3813
  }
3760
3814
  __name(applyElementGetData, "applyElementGetData");
3761
- async function callbackFn(elements, elementName, callbackName, data) {
3762
- const element = elements.find(
3763
- (el) => el.uiConfig && el.uiConfig.name === elementName
3815
+ var ITERATOR_SCOPED_ELEMENT = /^([^[]+)\[(\d+)\]:(.+)$/;
3816
+ async function callbackFn(elements, elementPath, callbackName, data) {
3817
+ const scoped = ITERATOR_SCOPED_ELEMENT.exec(elementPath);
3818
+ let searchScope = elements;
3819
+ let elementName = elementPath;
3820
+ let iteratorName = null;
3821
+ if (scoped) {
3822
+ iteratorName = scoped[1];
3823
+ elementName = scoped[3];
3824
+ const iter = elements.find(
3825
+ (el) => el?.uiConfig?.__type === "ui.iterator" && el.uiConfig.name === iteratorName
3826
+ );
3827
+ if (!iter) {
3828
+ throw new Error(`Iterator with name ${iteratorName} not found`);
3829
+ }
3830
+ searchScope = iter.uiConfig.content;
3831
+ }
3832
+ const element = searchScope.find(
3833
+ (el) => el?.uiConfig && el.uiConfig.name === elementName
3764
3834
  );
3765
3835
  if (!element) {
3766
- throw new Error(`Element with name ${elementName} not found`);
3836
+ const where = iteratorName ? ` in iterator ${iteratorName}` : "";
3837
+ throw new Error(`Element with name ${elementName} not found${where}`);
3767
3838
  }
3768
3839
  const cb = element[callbackName];
3769
3840
  if (typeof cb !== "function") {
@@ -4704,6 +4775,19 @@ var defaultOpts = {
4704
4775
  retries: 4,
4705
4776
  timeout: 6e4
4706
4777
  };
4778
+ var STEP_VALUE_WARN_BYTES = 1024 * 1024;
4779
+ function serializeStepValue(span, name, value) {
4780
+ const serialized = JSON.stringify(value);
4781
+ const bytes = serialized == null ? 0 : Buffer.byteLength(serialized, "utf8");
4782
+ span.setAttribute("step.value.bytes", bytes);
4783
+ if (bytes > STEP_VALUE_WARN_BYTES) {
4784
+ console.warn(
4785
+ `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.`
4786
+ );
4787
+ }
4788
+ return serialized;
4789
+ }
4790
+ __name(serializeStepValue, "serializeStepValue");
4707
4791
  async function insertNewStep(db, runId, name, stage) {
4708
4792
  await db.transaction().execute(async (trx) => {
4709
4793
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
@@ -4724,6 +4808,30 @@ async function insertNewStep(db, runId, name, stage) {
4724
4808
  __name(insertNewStep, "insertNewStep");
4725
4809
  function createFlowContext(runId, data, action, callback, element, spanId, ctx) {
4726
4810
  const usedNames = /* @__PURE__ */ new Set();
4811
+ let stepsSnapshot = null;
4812
+ let uiBoundaryCrossed = false;
4813
+ async function loadStepsSnapshot(db) {
4814
+ if (stepsSnapshot) {
4815
+ return stepsSnapshot;
4816
+ }
4817
+ const rows = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).select(["id", "name", "status", "type", "action"]).select(import_kysely6.sql`value::text`.as("valueRaw")).execute();
4818
+ const map = /* @__PURE__ */ new Map();
4819
+ for (const row of rows) {
4820
+ const existing = map.get(row.name);
4821
+ if (existing) {
4822
+ existing.push(row);
4823
+ } else {
4824
+ map.set(row.name, [row]);
4825
+ }
4826
+ }
4827
+ stepsSnapshot = map;
4828
+ return map;
4829
+ }
4830
+ __name(loadStepsSnapshot, "loadStepsSnapshot");
4831
+ async function readStepRows(db, name) {
4832
+ return await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).select(["id", "name", "status", "type", "action"]).select(import_kysely6.sql`value::text`.as("valueRaw")).execute();
4833
+ }
4834
+ __name(readStepRows, "readStepRows");
4727
4835
  const context7 = {
4728
4836
  identity: ctx.identity,
4729
4837
  env: ctx.env,
@@ -4763,7 +4871,15 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4763
4871
  throw new Error(`Duplicate step name: ${name}`);
4764
4872
  }
4765
4873
  usedNames.add(name);
4766
- const past = await db.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(import_kysely6.sql`value::text`.as("valueRaw")).execute();
4874
+ let past;
4875
+ if (uiBoundaryCrossed) {
4876
+ past = await readStepRows(db, name);
4877
+ } else {
4878
+ past = (await loadStepsSnapshot(db)).get(name) ?? [];
4879
+ if (!past.some((step) => step.status === "COMPLETED" /* COMPLETED */)) {
4880
+ past = await readStepRows(db, name);
4881
+ }
4882
+ }
4767
4883
  const newSteps = past.filter(
4768
4884
  (step) => step.status === "NEW" /* NEW */
4769
4885
  );
@@ -4846,7 +4962,7 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4846
4962
  }
4847
4963
  await db.updateTable("keel.flow_step").set({
4848
4964
  status: "COMPLETED" /* COMPLETED */,
4849
- value: JSON.stringify(result),
4965
+ value: serializeStepValue(span, name, result),
4850
4966
  spanId,
4851
4967
  endTime: /* @__PURE__ */ new Date()
4852
4968
  }).where("id", "=", newSteps[0].id).returningAll().executeTakeFirst();
@@ -4884,6 +5000,20 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4884
5000
  throw new Error(`Duplicate step name: ${name}`);
4885
5001
  }
4886
5002
  usedNames.add(name);
5003
+ const replayCompletedUiStep = /* @__PURE__ */ __name(async (completed) => {
5004
+ span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
5005
+ span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
5006
+ const rawValue = completed.valueRaw;
5007
+ const storedData = rawValue == null ? null : JSON.parse(rawValue);
5008
+ const parsedData2 = await applyElementGetData(
5009
+ options.content,
5010
+ transformRichDataTypes(storedData)
5011
+ );
5012
+ if (completed.action) {
5013
+ return { data: parsedData2, action: completed.action };
5014
+ }
5015
+ return parsedData2;
5016
+ }, "replayCompletedUiStep");
4887
5017
  const { step, inserted } = await db.transaction().execute(async (trx) => {
4888
5018
  await trx.selectFrom("keel.flow_run").select("id").where("id", "=", runId).forUpdate().executeTakeFirst();
4889
5019
  const existing = await trx.selectFrom("keel.flow_step").where("run_id", "=", runId).where("name", "=", name).selectAll().select(import_kysely6.sql`value::text`.as("valueRaw")).executeTakeFirst();
@@ -4902,18 +5032,8 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4902
5032
  return { step: created, inserted: true };
4903
5033
  });
4904
5034
  if (step && step.status === "COMPLETED" /* COMPLETED */) {
4905
- span.setAttribute(KEEL_INTERNAL_ATTR, KEEL_INTERNAL_CHILDREN);
4906
- span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
4907
- const rawValue = step.valueRaw;
4908
- const storedData = rawValue == null ? null : JSON.parse(rawValue);
4909
- const parsedData2 = await applyElementGetData(
4910
- options.content,
4911
- transformRichDataTypes(storedData)
4912
- );
4913
- if (step.action) {
4914
- return { data: parsedData2, action: step.action };
4915
- }
4916
- return parsedData2;
5035
+ uiBoundaryCrossed = true;
5036
+ return replayCompletedUiStep(step);
4917
5037
  }
4918
5038
  if (inserted) {
4919
5039
  span.setAttribute("rendered", true);
@@ -4972,12 +5092,13 @@ function createFlowContext(runId, data, action, callback, element, spanId, ctx)
4972
5092
  }
4973
5093
  await db.updateTable("keel.flow_step").set({
4974
5094
  status: "COMPLETED" /* COMPLETED */,
4975
- value: JSON.stringify(data),
5095
+ value: serializeStepValue(span, name, data),
4976
5096
  action,
4977
5097
  spanId,
4978
5098
  endTime: /* @__PURE__ */ new Date()
4979
5099
  }).where("id", "=", step.id).returningAll().executeTakeFirst();
4980
5100
  span.setAttribute("step.status", "COMPLETED" /* COMPLETED */);
5101
+ uiBoundaryCrossed = true;
4981
5102
  const parsedData = await applyElementGetData(
4982
5103
  options.content,
4983
5104
  transformRichDataTypes(data)
@@ -5382,7 +5503,7 @@ async function notifyEmail(input) {
5382
5503
  };
5383
5504
  const response = await fetch(`${getApiUrl4()}/notifications/json/email`, {
5384
5505
  method: "POST",
5385
- headers: { "Content-Type": "application/json" },
5506
+ headers: buildServiceAuthHeaders("notifications"),
5386
5507
  body: JSON.stringify(body)
5387
5508
  });
5388
5509
  if (!response.ok) {