@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.js CHANGED
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/engine.ts
2
9
  import { FlowSchema } from "@objectstack/spec/automation";
3
10
  var AutomationEngine = class {
@@ -426,7 +433,38 @@ var AutomationEngine = class {
426
433
  * boolean literals (true, false), and basic arithmetic.
427
434
  */
428
435
  evaluateCondition(expression, variables) {
429
- let resolved = expression;
436
+ const isEnvelope = typeof expression === "object" && expression != null && "dialect" in expression;
437
+ const dialect = isEnvelope ? expression.dialect : void 0;
438
+ const exprStr = typeof expression === "string" ? expression : expression?.source ?? "";
439
+ if (isEnvelope && dialect && dialect !== "cel" && dialect !== "flow" && dialect !== "template") {
440
+ return false;
441
+ }
442
+ if (dialect === "cel" || isEnvelope && !dialect) {
443
+ try {
444
+ const { ExpressionEngine } = __require("@objectstack/formula");
445
+ const vars = {};
446
+ for (const [key, value] of variables) {
447
+ const segs = key.split(".");
448
+ let cursor = vars;
449
+ for (let i = 0; i < segs.length - 1; i++) {
450
+ if (typeof cursor[segs[i]] !== "object" || cursor[segs[i]] === null) {
451
+ cursor[segs[i]] = {};
452
+ }
453
+ cursor = cursor[segs[i]];
454
+ }
455
+ cursor[segs[segs.length - 1]] = value;
456
+ }
457
+ const result = ExpressionEngine.evaluate(
458
+ { dialect: "cel", source: exprStr },
459
+ { extra: { vars }, record: vars }
460
+ );
461
+ if (!result.ok) return false;
462
+ return Boolean(result.value);
463
+ } catch {
464
+ return false;
465
+ }
466
+ }
467
+ let resolved = exprStr;
430
468
  for (const [key, value] of variables) {
431
469
  resolved = resolved.split(`{${key}}`).join(String(value));
432
470
  }
@@ -607,6 +645,9 @@ var AutomationServicePlugin = class {
607
645
  this.name = "com.objectstack.service-automation";
608
646
  this.version = "1.0.0";
609
647
  this.type = "standard";
648
+ // Soft dependency on metadata: we look it up at start() and tolerate absence.
649
+ // Do NOT declare a hard kernel dependency, so this plugin works in environments
650
+ // where MetadataPlugin is not registered.
610
651
  this.dependencies = [];
611
652
  this.options = options;
612
653
  }
@@ -621,18 +662,149 @@ var AutomationServicePlugin = class {
621
662
  ctx.logger.info("[Automation] Engine initialized");
622
663
  }
623
664
  async start(ctx) {
624
- if (!this.engine) return;
665
+ console.warn("[Automation:start] entering start()");
666
+ if (!this.engine) {
667
+ console.warn("[Automation:start] engine missing, bailing");
668
+ return;
669
+ }
625
670
  await ctx.trigger("automation:ready", this.engine);
626
671
  const nodeTypes = this.engine.getRegisteredNodeTypes();
627
672
  ctx.logger.info(
628
673
  `[Automation] Engine started with ${nodeTypes.length} node types: ${nodeTypes.join(", ") || "(none)"}`
629
674
  );
675
+ try {
676
+ const ql = ctx.getService("objectql");
677
+ if (!ql) {
678
+ console.warn("[Automation] objectql service not found at start()");
679
+ } else if (!ql.registry) {
680
+ console.warn("[Automation] objectql.registry is undefined at start()");
681
+ } else if (typeof ql.registry.listItems !== "function") {
682
+ console.warn("[Automation] objectql.registry.listItems is not a function");
683
+ }
684
+ const flows = ql?.registry?.listItems?.("flow") ?? [];
685
+ console.warn(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
686
+ let registered = 0;
687
+ for (const f of flows) {
688
+ const def = f;
689
+ if (!def?.name) continue;
690
+ try {
691
+ this.engine.registerFlow(def.name, def);
692
+ registered++;
693
+ } catch (e) {
694
+ const msg = e instanceof Error ? e.message : String(e);
695
+ ctx.logger.warn(`[Automation] failed to register flow ${def.name}: ${msg}`);
696
+ }
697
+ }
698
+ if (registered > 0) {
699
+ ctx.logger.info(`[Automation] Pulled ${registered} flow(s) from ObjectQL registry`);
700
+ }
701
+ } catch (err) {
702
+ const msg = err instanceof Error ? err.message : String(err);
703
+ ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
704
+ }
630
705
  }
631
706
  async destroy() {
632
707
  this.engine = void 0;
633
708
  }
634
709
  };
635
710
 
711
+ // src/plugins/template.ts
712
+ function resolvePath(base, path) {
713
+ let cur = base;
714
+ for (const seg of path) {
715
+ if (cur == null) return void 0;
716
+ if (typeof cur !== "object") return void 0;
717
+ cur = cur[seg];
718
+ }
719
+ return cur;
720
+ }
721
+ function resolveToken(token, variables, context) {
722
+ const trimmed = token.trim();
723
+ if (!trimmed) return void 0;
724
+ const dateFnMatch = /^(NOW|TODAY)\s*\(\s*\)\s*(?:([+\-])\s*(\S+))?$/.exec(trimmed);
725
+ if (dateFnMatch) {
726
+ const fn = dateFnMatch[1];
727
+ const sign = dateFnMatch[2] === "-" ? -1 : 1;
728
+ const offsetRaw = dateFnMatch[3];
729
+ let offset = 0;
730
+ if (offsetRaw) {
731
+ const asNum = Number(offsetRaw);
732
+ if (!isNaN(asNum)) {
733
+ offset = asNum;
734
+ } else if (variables.has(offsetRaw)) {
735
+ offset = Number(variables.get(offsetRaw)) || 0;
736
+ }
737
+ }
738
+ const now = /* @__PURE__ */ new Date();
739
+ if (offset) now.setDate(now.getDate() + sign * offset);
740
+ if (fn === "NOW") return now.toISOString();
741
+ return now.toISOString().slice(0, 10);
742
+ }
743
+ if (trimmed.startsWith("$User.")) {
744
+ const path = trimmed.slice("$User.".length).split(".");
745
+ if (path[0] === "Id") return context.userId;
746
+ if (path[0] === "Email") return resolvePath(context.user, ["email", ...path.slice(1)]) ?? void 0;
747
+ return resolvePath(context.user, path);
748
+ }
749
+ if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(trimmed)) {
750
+ const segments = trimmed.split(".");
751
+ const head = segments[0];
752
+ if (variables.has(head)) {
753
+ return resolvePath(variables.get(head), segments.slice(1));
754
+ }
755
+ if (variables.has(trimmed)) return variables.get(trimmed);
756
+ return void 0;
757
+ }
758
+ if (!/^[\w\s+\-*/%().,?:<>=!&|"'$]+$/.test(trimmed)) return void 0;
759
+ let safe = trimmed;
760
+ safe = safe.replace(/([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)/g, (match) => {
761
+ if (match === "true" || match === "false" || match === "null" || match === "undefined") return match;
762
+ const segs = match.split(".");
763
+ const head = segs[0];
764
+ let val;
765
+ if (variables.has(head)) val = resolvePath(variables.get(head), segs.slice(1));
766
+ else if (variables.has(match)) val = variables.get(match);
767
+ if (val === void 0 || val === null) return "null";
768
+ if (typeof val === "number" || typeof val === "boolean") return String(val);
769
+ return JSON.stringify(String(val));
770
+ });
771
+ try {
772
+ const fn = new Function(`"use strict"; return (${safe});`);
773
+ return fn();
774
+ } catch {
775
+ return void 0;
776
+ }
777
+ }
778
+ function interpolateString(input, variables, context) {
779
+ if (!input.includes("{")) return input;
780
+ const single = /^\{([^{}]+)\}$/.exec(input);
781
+ if (single) {
782
+ const value = resolveToken(single[1], variables, context);
783
+ return value;
784
+ }
785
+ return input.replace(/\{([^{}]+)\}/g, (_match, expr) => {
786
+ const value = resolveToken(expr, variables, context);
787
+ if (value === void 0 || value === null) return "";
788
+ return String(value);
789
+ });
790
+ }
791
+ function interpolate(value, variables, context) {
792
+ if (typeof value === "string") {
793
+ return interpolateString(value, variables, context);
794
+ }
795
+ if (Array.isArray(value)) {
796
+ return value.map((v) => interpolate(v, variables, context));
797
+ }
798
+ if (value && typeof value === "object") {
799
+ const out = {};
800
+ for (const [k, v] of Object.entries(value)) {
801
+ out[k] = interpolate(v, variables, context);
802
+ }
803
+ return out;
804
+ }
805
+ return value;
806
+ }
807
+
636
808
  // src/plugins/crud-nodes-plugin.ts
637
809
  var CrudNodesPlugin = class {
638
810
  constructor() {
@@ -643,39 +815,105 @@ var CrudNodesPlugin = class {
643
815
  }
644
816
  async init(ctx) {
645
817
  const engine = ctx.getService("automation");
818
+ const getData = () => {
819
+ try {
820
+ return ctx.getService("data") ?? ctx.getService("objectql");
821
+ } catch {
822
+ return void 0;
823
+ }
824
+ };
646
825
  engine.registerNodeExecutor({
647
826
  type: "get_record",
648
- async execute(node, _variables, _context) {
649
- const config = node.config;
650
- return {
651
- success: true,
652
- output: { records: [], object: config?.object }
653
- };
827
+ async execute(node, variables, context) {
828
+ const cfg = node.config ?? {};
829
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
830
+ if (!objectName) return { success: false, error: "get_record: objectName required" };
831
+ const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
832
+ const fields = cfg.fields;
833
+ const limit = typeof cfg.limit === "number" ? cfg.limit : void 0;
834
+ const outputVariable = cfg.outputVariable;
835
+ const data = getData();
836
+ if (!data) {
837
+ ctx.logger.warn(`[get_record] no data engine; skipping ${objectName}`);
838
+ return { success: true, output: { records: [], object: objectName } };
839
+ }
840
+ try {
841
+ if (limit && limit > 1) {
842
+ const records = await data.find(objectName, { where: filter, fields, limit });
843
+ if (outputVariable) variables.set(outputVariable, records);
844
+ return { success: true, output: { records, object: objectName } };
845
+ }
846
+ const record = await data.findOne(objectName, { where: filter, fields });
847
+ if (outputVariable) variables.set(outputVariable, record);
848
+ return { success: true, output: { record, id: record?.id, object: objectName } };
849
+ } catch (err) {
850
+ return { success: false, error: `get_record(${objectName}) failed: ${err.message}` };
851
+ }
654
852
  }
655
853
  });
656
854
  engine.registerNodeExecutor({
657
855
  type: "create_record",
658
- async execute(node, _variables, _context) {
659
- const config = node.config;
660
- return {
661
- success: true,
662
- output: { id: "new-record-id", object: config?.object }
663
- };
856
+ async execute(node, variables, context) {
857
+ const cfg = node.config ?? {};
858
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
859
+ if (!objectName) return { success: false, error: "create_record: objectName required" };
860
+ const fields = interpolate(cfg.fields ?? {}, variables, context);
861
+ const outputVariable = cfg.outputVariable;
862
+ const data = getData();
863
+ if (!data) {
864
+ ctx.logger.warn(`[create_record] no data engine; skipping ${objectName}`);
865
+ if (outputVariable) variables.set(outputVariable, `mock-${objectName}-${Date.now()}`);
866
+ return { success: true, output: { id: `mock-${objectName}-${Date.now()}`, object: objectName } };
867
+ }
868
+ try {
869
+ const created = await data.insert(objectName, fields);
870
+ const insertedId = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
871
+ if (outputVariable) variables.set(outputVariable, insertedId);
872
+ return { success: true, output: { id: insertedId, record: created, object: objectName } };
873
+ } catch (err) {
874
+ return { success: false, error: `create_record(${objectName}) failed: ${err.message}` };
875
+ }
664
876
  }
665
877
  });
666
878
  engine.registerNodeExecutor({
667
879
  type: "update_record",
668
- async execute(_node, _variables, _context) {
669
- return { success: true };
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: "update_record: objectName required" };
884
+ const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
885
+ const fields = interpolate(cfg.fields ?? {}, variables, context);
886
+ const data = getData();
887
+ if (!data) {
888
+ ctx.logger.warn(`[update_record] no data engine; skipping ${objectName}`);
889
+ return { success: true };
890
+ }
891
+ try {
892
+ const result = await data.update(objectName, fields, { where: filter });
893
+ return { success: true, output: { result, object: objectName } };
894
+ } catch (err) {
895
+ return { success: false, error: `update_record(${objectName}) failed: ${err.message}` };
896
+ }
670
897
  }
671
898
  });
672
899
  engine.registerNodeExecutor({
673
900
  type: "delete_record",
674
- async execute(_node, _variables, _context) {
675
- return { success: true };
901
+ async execute(node, variables, context) {
902
+ const cfg = node.config ?? {};
903
+ const objectName = String(cfg.objectName ?? cfg.object ?? "");
904
+ if (!objectName) return { success: false, error: "delete_record: objectName required" };
905
+ const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
906
+ const data = getData();
907
+ if (!data) return { success: true };
908
+ try {
909
+ const result = await data.delete(objectName, { where: filter });
910
+ return { success: true, output: { result, object: objectName } };
911
+ } catch (err) {
912
+ return { success: false, error: `delete_record(${objectName}) failed: ${err.message}` };
913
+ }
676
914
  }
677
915
  });
678
- ctx.logger.info("[CRUD Nodes] 4 node executors registered");
916
+ ctx.logger.info("[CRUD Nodes] 4 node executors registered (data-backed)");
679
917
  }
680
918
  };
681
919
 
@@ -781,11 +1019,54 @@ var HttpConnectorPlugin = class {
781
1019
  ctx.logger.info("[HTTP Connector] 2 node executors registered");
782
1020
  }
783
1021
  };
1022
+
1023
+ // src/plugins/screen-nodes-plugin.ts
1024
+ var ScreenNodesPlugin = class {
1025
+ constructor() {
1026
+ this.name = "com.objectstack.automation.screen-nodes";
1027
+ this.version = "1.0.0";
1028
+ this.type = "standard";
1029
+ this.dependencies = ["com.objectstack.service-automation"];
1030
+ }
1031
+ async init(ctx) {
1032
+ const engine = ctx.getService("automation");
1033
+ engine.registerNodeExecutor({
1034
+ type: "screen",
1035
+ async execute(_node, _variables, _context) {
1036
+ return { success: true };
1037
+ }
1038
+ });
1039
+ engine.registerNodeExecutor({
1040
+ type: "script",
1041
+ async execute(node, _variables, _context) {
1042
+ const cfg = node.config ?? {};
1043
+ const actionType = cfg.actionType ?? "noop";
1044
+ if (actionType === "email") {
1045
+ ctx.logger.info(
1046
+ `[Script:email] template=${String(cfg.template)} recipients=${JSON.stringify(cfg.recipients)} vars=${JSON.stringify(cfg.variables)}`
1047
+ );
1048
+ return {
1049
+ success: true,
1050
+ output: {
1051
+ actionType,
1052
+ template: cfg.template,
1053
+ recipients: cfg.recipients
1054
+ }
1055
+ };
1056
+ }
1057
+ ctx.logger.info(`[Script:${actionType}] node=${node.id} executed (no-op handler)`);
1058
+ return { success: true, output: { actionType } };
1059
+ }
1060
+ });
1061
+ ctx.logger.info("[Screen/Script Nodes] 2 node executors registered");
1062
+ }
1063
+ };
784
1064
  export {
785
1065
  AutomationEngine,
786
1066
  AutomationServicePlugin,
787
1067
  CrudNodesPlugin,
788
1068
  HttpConnectorPlugin,
789
- LogicNodesPlugin
1069
+ LogicNodesPlugin,
1070
+ ScreenNodesPlugin
790
1071
  };
791
1072
  //# sourceMappingURL=index.js.map