@objectstack/service-automation 4.0.4 → 4.0.5

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
@@ -24,7 +24,8 @@ __export(index_exports, {
24
24
  AutomationServicePlugin: () => AutomationServicePlugin,
25
25
  CrudNodesPlugin: () => CrudNodesPlugin,
26
26
  HttpConnectorPlugin: () => HttpConnectorPlugin,
27
- LogicNodesPlugin: () => LogicNodesPlugin
27
+ LogicNodesPlugin: () => LogicNodesPlugin,
28
+ ScreenNodesPlugin: () => ScreenNodesPlugin
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
30
31
 
@@ -456,7 +457,38 @@ var AutomationEngine = class {
456
457
  * boolean literals (true, false), and basic arithmetic.
457
458
  */
458
459
  evaluateCondition(expression, variables) {
459
- let resolved = expression;
460
+ const isEnvelope = typeof expression === "object" && expression != null && "dialect" in expression;
461
+ const dialect = isEnvelope ? expression.dialect : void 0;
462
+ const exprStr = typeof expression === "string" ? expression : expression?.source ?? "";
463
+ if (isEnvelope && dialect && dialect !== "cel" && dialect !== "flow" && dialect !== "template") {
464
+ return false;
465
+ }
466
+ if (dialect === "cel" || isEnvelope && !dialect) {
467
+ try {
468
+ const { ExpressionEngine } = require("@objectstack/formula");
469
+ const vars = {};
470
+ for (const [key, value] of variables) {
471
+ const segs = key.split(".");
472
+ let cursor = vars;
473
+ for (let i = 0; i < segs.length - 1; i++) {
474
+ if (typeof cursor[segs[i]] !== "object" || cursor[segs[i]] === null) {
475
+ cursor[segs[i]] = {};
476
+ }
477
+ cursor = cursor[segs[i]];
478
+ }
479
+ cursor[segs[segs.length - 1]] = value;
480
+ }
481
+ const result = ExpressionEngine.evaluate(
482
+ { dialect: "cel", source: exprStr },
483
+ { extra: { vars }, record: vars }
484
+ );
485
+ if (!result.ok) return false;
486
+ return Boolean(result.value);
487
+ } catch {
488
+ return false;
489
+ }
490
+ }
491
+ let resolved = exprStr;
460
492
  for (const [key, value] of variables) {
461
493
  resolved = resolved.split(`{${key}}`).join(String(value));
462
494
  }
@@ -637,6 +669,9 @@ var AutomationServicePlugin = class {
637
669
  this.name = "com.objectstack.service-automation";
638
670
  this.version = "1.0.0";
639
671
  this.type = "standard";
672
+ // Soft dependency on metadata: we look it up at start() and tolerate absence.
673
+ // Do NOT declare a hard kernel dependency, so this plugin works in environments
674
+ // where MetadataPlugin is not registered.
640
675
  this.dependencies = [];
641
676
  this.options = options;
642
677
  }
@@ -651,18 +686,149 @@ var AutomationServicePlugin = class {
651
686
  ctx.logger.info("[Automation] Engine initialized");
652
687
  }
653
688
  async start(ctx) {
654
- if (!this.engine) return;
689
+ console.warn("[Automation:start] entering start()");
690
+ if (!this.engine) {
691
+ console.warn("[Automation:start] engine missing, bailing");
692
+ return;
693
+ }
655
694
  await ctx.trigger("automation:ready", this.engine);
656
695
  const nodeTypes = this.engine.getRegisteredNodeTypes();
657
696
  ctx.logger.info(
658
697
  `[Automation] Engine started with ${nodeTypes.length} node types: ${nodeTypes.join(", ") || "(none)"}`
659
698
  );
699
+ try {
700
+ const ql = ctx.getService("objectql");
701
+ if (!ql) {
702
+ console.warn("[Automation] objectql service not found at start()");
703
+ } else if (!ql.registry) {
704
+ console.warn("[Automation] objectql.registry is undefined at start()");
705
+ } else if (typeof ql.registry.listItems !== "function") {
706
+ console.warn("[Automation] objectql.registry.listItems is not a function");
707
+ }
708
+ const flows = ql?.registry?.listItems?.("flow") ?? [];
709
+ console.warn(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
710
+ let registered = 0;
711
+ for (const f of flows) {
712
+ const def = f;
713
+ if (!def?.name) continue;
714
+ try {
715
+ this.engine.registerFlow(def.name, def);
716
+ registered++;
717
+ } catch (e) {
718
+ const msg = e instanceof Error ? e.message : String(e);
719
+ ctx.logger.warn(`[Automation] failed to register flow ${def.name}: ${msg}`);
720
+ }
721
+ }
722
+ if (registered > 0) {
723
+ ctx.logger.info(`[Automation] Pulled ${registered} flow(s) from ObjectQL registry`);
724
+ }
725
+ } catch (err) {
726
+ const msg = err instanceof Error ? err.message : String(err);
727
+ ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
728
+ }
660
729
  }
661
730
  async destroy() {
662
731
  this.engine = void 0;
663
732
  }
664
733
  };
665
734
 
735
+ // src/plugins/template.ts
736
+ function resolvePath(base, path) {
737
+ let cur = base;
738
+ for (const seg of path) {
739
+ if (cur == null) return void 0;
740
+ if (typeof cur !== "object") return void 0;
741
+ cur = cur[seg];
742
+ }
743
+ return cur;
744
+ }
745
+ function resolveToken(token, variables, context) {
746
+ const trimmed = token.trim();
747
+ if (!trimmed) return void 0;
748
+ const dateFnMatch = /^(NOW|TODAY)\s*\(\s*\)\s*(?:([+\-])\s*(\S+))?$/.exec(trimmed);
749
+ if (dateFnMatch) {
750
+ const fn = dateFnMatch[1];
751
+ const sign = dateFnMatch[2] === "-" ? -1 : 1;
752
+ const offsetRaw = dateFnMatch[3];
753
+ let offset = 0;
754
+ if (offsetRaw) {
755
+ const asNum = Number(offsetRaw);
756
+ if (!isNaN(asNum)) {
757
+ offset = asNum;
758
+ } else if (variables.has(offsetRaw)) {
759
+ offset = Number(variables.get(offsetRaw)) || 0;
760
+ }
761
+ }
762
+ const now = /* @__PURE__ */ new Date();
763
+ if (offset) now.setDate(now.getDate() + sign * offset);
764
+ if (fn === "NOW") return now.toISOString();
765
+ return now.toISOString().slice(0, 10);
766
+ }
767
+ if (trimmed.startsWith("$User.")) {
768
+ const path = trimmed.slice("$User.".length).split(".");
769
+ if (path[0] === "Id") return context.userId;
770
+ if (path[0] === "Email") return resolvePath(context.user, ["email", ...path.slice(1)]) ?? void 0;
771
+ return resolvePath(context.user, path);
772
+ }
773
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(trimmed)) {
774
+ const segments = trimmed.split(".");
775
+ const head = segments[0];
776
+ if (variables.has(head)) {
777
+ return resolvePath(variables.get(head), segments.slice(1));
778
+ }
779
+ if (variables.has(trimmed)) return variables.get(trimmed);
780
+ return void 0;
781
+ }
782
+ if (!/^[\w\s+\-*/%().,?:<>=!&|"'$]+$/.test(trimmed)) return void 0;
783
+ let safe = trimmed;
784
+ safe = safe.replace(/([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g, (match) => {
785
+ if (match === "true" || match === "false" || match === "null" || match === "undefined") return match;
786
+ const segs = match.split(".");
787
+ const head = segs[0];
788
+ let val;
789
+ if (variables.has(head)) val = resolvePath(variables.get(head), segs.slice(1));
790
+ else if (variables.has(match)) val = variables.get(match);
791
+ if (val === void 0 || val === null) return "null";
792
+ if (typeof val === "number" || typeof val === "boolean") return String(val);
793
+ return JSON.stringify(String(val));
794
+ });
795
+ try {
796
+ const fn = new Function(`"use strict"; return (${safe});`);
797
+ return fn();
798
+ } catch {
799
+ return void 0;
800
+ }
801
+ }
802
+ function interpolateString(input, variables, context) {
803
+ if (!input.includes("{")) return input;
804
+ const single = /^\{([^{}]+)\}$/.exec(input);
805
+ if (single) {
806
+ const value = resolveToken(single[1], variables, context);
807
+ return value;
808
+ }
809
+ return input.replace(/\{([^{}]+)\}/g, (_match, expr) => {
810
+ const value = resolveToken(expr, variables, context);
811
+ if (value === void 0 || value === null) return "";
812
+ return String(value);
813
+ });
814
+ }
815
+ function interpolate(value, variables, context) {
816
+ if (typeof value === "string") {
817
+ return interpolateString(value, variables, context);
818
+ }
819
+ if (Array.isArray(value)) {
820
+ return value.map((v) => interpolate(v, variables, context));
821
+ }
822
+ if (value && typeof value === "object") {
823
+ const out = {};
824
+ for (const [k, v] of Object.entries(value)) {
825
+ out[k] = interpolate(v, variables, context);
826
+ }
827
+ return out;
828
+ }
829
+ return value;
830
+ }
831
+
666
832
  // src/plugins/crud-nodes-plugin.ts
667
833
  var CrudNodesPlugin = class {
668
834
  constructor() {
@@ -673,39 +839,105 @@ var CrudNodesPlugin = class {
673
839
  }
674
840
  async init(ctx) {
675
841
  const engine = ctx.getService("automation");
842
+ const getData = () => {
843
+ try {
844
+ return ctx.getService("data") ?? ctx.getService("objectql");
845
+ } catch {
846
+ return void 0;
847
+ }
848
+ };
676
849
  engine.registerNodeExecutor({
677
850
  type: "get_record",
678
- async execute(node, _variables, _context) {
679
- const config = node.config;
680
- return {
681
- success: true,
682
- output: { records: [], object: config?.object }
683
- };
851
+ async execute(node, variables, context) {
852
+ const cfg = node.config ?? {};
853
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
854
+ if (!objectName) return { success: false, error: "get_record: objectName required" };
855
+ const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
856
+ const fields = cfg.fields;
857
+ const limit = typeof cfg.limit === "number" ? cfg.limit : void 0;
858
+ const outputVariable = cfg.outputVariable;
859
+ const data = getData();
860
+ if (!data) {
861
+ ctx.logger.warn(`[get_record] no data engine; skipping ${objectName}`);
862
+ return { success: true, output: { records: [], object: objectName } };
863
+ }
864
+ try {
865
+ if (limit && limit > 1) {
866
+ const records = await data.find(objectName, { where: filter, fields, limit });
867
+ if (outputVariable) variables.set(outputVariable, records);
868
+ return { success: true, output: { records, object: objectName } };
869
+ }
870
+ const record = await data.findOne(objectName, { where: filter, fields });
871
+ if (outputVariable) variables.set(outputVariable, record);
872
+ return { success: true, output: { record, id: record?.id, object: objectName } };
873
+ } catch (err) {
874
+ return { success: false, error: `get_record(${objectName}) failed: ${err.message}` };
875
+ }
684
876
  }
685
877
  });
686
878
  engine.registerNodeExecutor({
687
879
  type: "create_record",
688
- async execute(node, _variables, _context) {
689
- const config = node.config;
690
- return {
691
- success: true,
692
- output: { id: "new-record-id", object: config?.object }
693
- };
880
+ async execute(node, variables, context) {
881
+ const cfg = node.config ?? {};
882
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
883
+ if (!objectName) return { success: false, error: "create_record: objectName required" };
884
+ const fields = interpolate(cfg.fields ?? {}, variables, context);
885
+ const outputVariable = cfg.outputVariable;
886
+ const data = getData();
887
+ if (!data) {
888
+ ctx.logger.warn(`[create_record] no data engine; skipping ${objectName}`);
889
+ if (outputVariable) variables.set(outputVariable, `mock-${objectName}-${Date.now()}`);
890
+ return { success: true, output: { id: `mock-${objectName}-${Date.now()}`, object: objectName } };
891
+ }
892
+ try {
893
+ const created = await data.insert(objectName, fields);
894
+ const insertedId = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
895
+ if (outputVariable) variables.set(outputVariable, insertedId);
896
+ return { success: true, output: { id: insertedId, record: created, object: objectName } };
897
+ } catch (err) {
898
+ return { success: false, error: `create_record(${objectName}) failed: ${err.message}` };
899
+ }
694
900
  }
695
901
  });
696
902
  engine.registerNodeExecutor({
697
903
  type: "update_record",
698
- async execute(_node, _variables, _context) {
699
- return { success: true };
904
+ async execute(node, variables, context) {
905
+ const cfg = node.config ?? {};
906
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
907
+ if (!objectName) return { success: false, error: "update_record: objectName required" };
908
+ const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
909
+ const fields = interpolate(cfg.fields ?? {}, variables, context);
910
+ const data = getData();
911
+ if (!data) {
912
+ ctx.logger.warn(`[update_record] no data engine; skipping ${objectName}`);
913
+ return { success: true };
914
+ }
915
+ try {
916
+ const result = await data.update(objectName, fields, { where: filter });
917
+ return { success: true, output: { result, object: objectName } };
918
+ } catch (err) {
919
+ return { success: false, error: `update_record(${objectName}) failed: ${err.message}` };
920
+ }
700
921
  }
701
922
  });
702
923
  engine.registerNodeExecutor({
703
924
  type: "delete_record",
704
- async execute(_node, _variables, _context) {
705
- return { success: true };
925
+ async execute(node, variables, context) {
926
+ const cfg = node.config ?? {};
927
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
928
+ if (!objectName) return { success: false, error: "delete_record: objectName required" };
929
+ const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
930
+ const data = getData();
931
+ if (!data) return { success: true };
932
+ try {
933
+ const result = await data.delete(objectName, { where: filter });
934
+ return { success: true, output: { result, object: objectName } };
935
+ } catch (err) {
936
+ return { success: false, error: `delete_record(${objectName}) failed: ${err.message}` };
937
+ }
706
938
  }
707
939
  });
708
- ctx.logger.info("[CRUD Nodes] 4 node executors registered");
940
+ ctx.logger.info("[CRUD Nodes] 4 node executors registered (data-backed)");
709
941
  }
710
942
  };
711
943
 
@@ -811,12 +1043,55 @@ var HttpConnectorPlugin = class {
811
1043
  ctx.logger.info("[HTTP Connector] 2 node executors registered");
812
1044
  }
813
1045
  };
1046
+
1047
+ // src/plugins/screen-nodes-plugin.ts
1048
+ var ScreenNodesPlugin = class {
1049
+ constructor() {
1050
+ this.name = "com.objectstack.automation.screen-nodes";
1051
+ this.version = "1.0.0";
1052
+ this.type = "standard";
1053
+ this.dependencies = ["com.objectstack.service-automation"];
1054
+ }
1055
+ async init(ctx) {
1056
+ const engine = ctx.getService("automation");
1057
+ engine.registerNodeExecutor({
1058
+ type: "screen",
1059
+ async execute(_node, _variables, _context) {
1060
+ return { success: true };
1061
+ }
1062
+ });
1063
+ engine.registerNodeExecutor({
1064
+ type: "script",
1065
+ async execute(node, _variables, _context) {
1066
+ const cfg = node.config ?? {};
1067
+ const actionType = cfg.actionType ?? "noop";
1068
+ if (actionType === "email") {
1069
+ ctx.logger.info(
1070
+ `[Script:email] template=${String(cfg.template)} recipients=${JSON.stringify(cfg.recipients)} vars=${JSON.stringify(cfg.variables)}`
1071
+ );
1072
+ return {
1073
+ success: true,
1074
+ output: {
1075
+ actionType,
1076
+ template: cfg.template,
1077
+ recipients: cfg.recipients
1078
+ }
1079
+ };
1080
+ }
1081
+ ctx.logger.info(`[Script:${actionType}] node=${node.id} executed (no-op handler)`);
1082
+ return { success: true, output: { actionType } };
1083
+ }
1084
+ });
1085
+ ctx.logger.info("[Screen/Script Nodes] 2 node executors registered");
1086
+ }
1087
+ };
814
1088
  // Annotate the CommonJS export names for ESM import in node:
815
1089
  0 && (module.exports = {
816
1090
  AutomationEngine,
817
1091
  AutomationServicePlugin,
818
1092
  CrudNodesPlugin,
819
1093
  HttpConnectorPlugin,
820
- LogicNodesPlugin
1094
+ LogicNodesPlugin,
1095
+ ScreenNodesPlugin
821
1096
  });
822
1097
  //# sourceMappingURL=index.cjs.map