@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 +296 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -7
- package/dist/index.d.ts +41 -7
- package/dist/index.js +301 -20
- package/dist/index.js.map +1 -1
- package/package.json +32 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -168
- package/src/engine.test.ts +0 -1425
- package/src/engine.ts +0 -807
- package/src/index.ts +0 -14
- package/src/plugin.ts +0 -80
- package/src/plugins/crud-nodes-plugin.ts +0 -68
- package/src/plugins/http-connector-plugin.ts +0 -70
- package/src/plugins/logic-nodes-plugin.ts +0 -67
- package/tsconfig.json +0 -17
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
|
-
|
|
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
|
-
|
|
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,
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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,
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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(
|
|
699
|
-
|
|
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(
|
|
705
|
-
|
|
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
|