@ductape/sdk 0.1.151 → 0.1.153
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/database/databases.service.js.map +1 -1
- package/dist/features/feature-executor.d.ts +1 -1
- package/dist/features/feature-executor.js +4 -1
- package/dist/features/feature-executor.js.map +1 -1
- package/dist/features/features.service.js +122 -17
- package/dist/features/features.service.js.map +1 -1
- package/dist/features/types/features.types.d.ts +38 -0
- package/dist/features/types/features.types.js.map +1 -1
- package/dist/processor/services/processor.service.js +1 -1
- package/dist/processor/services/processor.service.js.map +1 -1
- package/dist/resilience/fallback.service.d.ts +1 -0
- package/dist/resilience/fallback.service.js +4 -1
- package/dist/resilience/fallback.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1666,7 +1666,7 @@ function createStepOutputProxy(pathPrefix = '') {
|
|
|
1666
1666
|
* - Default delimiter is "" so template literals like `receipts/${ctx.input.orderId}.txt` become receipts/ORD-001.txt not receipts/ ORD-001 .txt.
|
|
1667
1667
|
* - Recurses into plain objects and arrays; leaves other primitives unchanged.
|
|
1668
1668
|
*/
|
|
1669
|
-
const OPERATOR_REF_IN_STRING_REGEX = /(\$Input\{[^}]+\}|\$Sequence\{main\}\{[^}]+\}\{[^}]*\}|\$Now)/g;
|
|
1669
|
+
const OPERATOR_REF_IN_STRING_REGEX = /(\$Input\{[^}]+\}|\$Sequence\{main\}\{[^}]+\}\{[^}]*\}|\$Now|\$Uuid)/g;
|
|
1670
1670
|
function convertStringToConcatIfOperatorRefs(str, delimiter = '') {
|
|
1671
1671
|
OPERATOR_REF_IN_STRING_REGEX.lastIndex = 0;
|
|
1672
1672
|
const tokens = str.split(OPERATOR_REF_IN_STRING_REGEX);
|
|
@@ -1674,7 +1674,7 @@ function convertStringToConcatIfOperatorRefs(str, delimiter = '') {
|
|
|
1674
1674
|
for (const t of tokens) {
|
|
1675
1675
|
if (!t)
|
|
1676
1676
|
continue;
|
|
1677
|
-
const isOp = t.startsWith('$Input{') || t.startsWith('$Sequence{') || t === '$Now';
|
|
1677
|
+
const isOp = t.startsWith('$Input{') || t.startsWith('$Sequence{') || t === '$Now' || t === '$Uuid';
|
|
1678
1678
|
if (isOp) {
|
|
1679
1679
|
parts.push(t);
|
|
1680
1680
|
}
|
|
@@ -1717,6 +1717,9 @@ function convertToOperatorInput(value, inputProxy, schemaKeys) {
|
|
|
1717
1717
|
return out;
|
|
1718
1718
|
}
|
|
1719
1719
|
if (typeof value === 'string') {
|
|
1720
|
+
if (/^\$(?:Concat|Substring|Trim|Uppercase|Lowercase|Dateformat|Replace|Split|Join|Pick|Filter|Find|Size|Length|Add|Subtract)\s*[({]/.test(value)) {
|
|
1721
|
+
return value;
|
|
1722
|
+
}
|
|
1720
1723
|
return convertStringToConcatIfOperatorRefs(value);
|
|
1721
1724
|
}
|
|
1722
1725
|
return value;
|
|
@@ -1725,19 +1728,28 @@ function convertToOperatorInput(value, inputProxy, schemaKeys) {
|
|
|
1725
1728
|
* Recording context that captures step definitions during handler execution.
|
|
1726
1729
|
* Uses operator strings ($Input{field}, $Sequence{main}{stepTag}{field}) per Ductape operator docs.
|
|
1727
1730
|
*/
|
|
1728
|
-
/** Build a
|
|
1731
|
+
/** Build a condition matching the concrete output used to record a branch. */
|
|
1729
1732
|
function buildConditionFromOverride(stepTag, override) {
|
|
1730
|
-
if (override === null || override === undefined)
|
|
1731
|
-
return '';
|
|
1732
|
-
if (typeof override !== 'object' || Array.isArray(override)) {
|
|
1733
|
-
return `$Step{${stepTag}}{value} == ${JSON.stringify(override)}`;
|
|
1734
|
-
}
|
|
1735
1733
|
const parts = [];
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1734
|
+
const visit = (value, path) => {
|
|
1735
|
+
const reference = `$Step{${stepTag}}{${path || 'value'}}`;
|
|
1736
|
+
if (Array.isArray(value)) {
|
|
1737
|
+
parts.push(`$Step{${stepTag}}{${path ? `${path}.length` : 'length'}} == ${value.length}`);
|
|
1738
|
+
return;
|
|
1739
1739
|
}
|
|
1740
|
-
|
|
1740
|
+
if (value === null || value === undefined) {
|
|
1741
|
+
parts.push(`${reference} == null`);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
if (typeof value !== 'object') {
|
|
1745
|
+
parts.push(`${reference} == ${JSON.stringify(value)}`);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1749
|
+
visit(nested, path ? `${path}.${key}` : key);
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
visit(override, '');
|
|
1741
1753
|
return parts.length === 0 ? '' : parts.join(' && ');
|
|
1742
1754
|
}
|
|
1743
1755
|
/** Build a step condition from a scenario (recordInput) so the step only runs when feature input matches (e.g. $Input{type} == 'a'). */
|
|
@@ -1767,6 +1779,8 @@ class RecordingContext {
|
|
|
1767
1779
|
/** When set (from recordScenarios), every step we push gets this condition so it only runs when input matches. */
|
|
1768
1780
|
this._scenarioCondition = null;
|
|
1769
1781
|
this._recordInput = null;
|
|
1782
|
+
/** Explicit portable control-flow condition inherited by steps in a branch. */
|
|
1783
|
+
this._portableCondition = null;
|
|
1770
1784
|
this._featureTag = featureTag;
|
|
1771
1785
|
this._inputSchemaKeys = featureInputSchema ? Object.keys(featureInputSchema) : [];
|
|
1772
1786
|
this._stepResultOverrides = stepResultOverrides !== null && stepResultOverrides !== void 0 ? stepResultOverrides : {};
|
|
@@ -1818,10 +1832,68 @@ class RecordingContext {
|
|
|
1818
1832
|
get now() {
|
|
1819
1833
|
return '$Now';
|
|
1820
1834
|
}
|
|
1835
|
+
conditionOperand(value) {
|
|
1836
|
+
const converted = convertToOperatorInput(value, this._inputProxy, this._inputSchemaKeys);
|
|
1837
|
+
if (typeof converted === 'string' && converted.startsWith('$'))
|
|
1838
|
+
return converted;
|
|
1839
|
+
return JSON.stringify(converted);
|
|
1840
|
+
}
|
|
1841
|
+
comparison(left, operator, inverseOperator, right) {
|
|
1842
|
+
const lhs = this.conditionOperand(left);
|
|
1843
|
+
const rhs = this.conditionOperand(right);
|
|
1844
|
+
return { expression: `${lhs} ${operator} ${rhs}`, inverse: `${lhs} ${inverseOperator} ${rhs}` };
|
|
1845
|
+
}
|
|
1846
|
+
get when() {
|
|
1847
|
+
return {
|
|
1848
|
+
eq: (left, right) => this.comparison(left, '==', '!=', right),
|
|
1849
|
+
ne: (left, right) => this.comparison(left, '!=', '==', right),
|
|
1850
|
+
gt: (left, right) => this.comparison(left, '>', '<=', right),
|
|
1851
|
+
gte: (left, right) => this.comparison(left, '>=', '<', right),
|
|
1852
|
+
lt: (left, right) => this.comparison(left, '<', '>=', right),
|
|
1853
|
+
lte: (left, right) => this.comparison(left, '<=', '>', right),
|
|
1854
|
+
truthy: (value) => this.comparison(value, '==', '!=', true),
|
|
1855
|
+
falsy: (value) => this.comparison(value, '==', '!=', false),
|
|
1856
|
+
and: (...conditions) => ({
|
|
1857
|
+
expression: conditions.map((condition) => `(${condition.expression})`).join(' && '),
|
|
1858
|
+
inverse: conditions.map((condition) => `(${condition.inverse})`).join(' || '),
|
|
1859
|
+
}),
|
|
1860
|
+
or: (...conditions) => ({
|
|
1861
|
+
expression: conditions.map((condition) => `(${condition.expression})`).join(' || '),
|
|
1862
|
+
inverse: conditions.map((condition) => `(${condition.inverse})`).join(' && '),
|
|
1863
|
+
}),
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
async branch(condition, handlers) {
|
|
1867
|
+
if (!(condition === null || condition === void 0 ? void 0 : condition.expression) || !(condition === null || condition === void 0 ? void 0 : condition.inverse)) {
|
|
1868
|
+
throw new FeatureCompilationError(this._featureTag, '(branch)', 'ctx.branch requires a condition created by ctx.when.');
|
|
1869
|
+
}
|
|
1870
|
+
const parent = this._portableCondition;
|
|
1871
|
+
const scoped = (expression) => parent ? `(${parent}) && (${expression})` : expression;
|
|
1872
|
+
try {
|
|
1873
|
+
this._portableCondition = scoped(condition.expression);
|
|
1874
|
+
await handlers.then();
|
|
1875
|
+
if (handlers.else) {
|
|
1876
|
+
this._portableCondition = scoped(condition.inverse);
|
|
1877
|
+
await handlers.else();
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
finally {
|
|
1881
|
+
this._portableCondition = parent;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
async each(items, handler) {
|
|
1885
|
+
if (!Array.isArray(items)) {
|
|
1886
|
+
throw new FeatureCompilationError(this._featureTag, '(loop)', 'ctx.each requires a concrete ctx.sampleInput array during compilation. Use ctx.functions for runtime-sized collection logic.');
|
|
1887
|
+
}
|
|
1888
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
1889
|
+
await handler(items[index], index);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1821
1892
|
/**
|
|
1822
1893
|
* Record a step definition
|
|
1823
1894
|
*/
|
|
1824
1895
|
async step(tag, handler, rollback, options) {
|
|
1896
|
+
var _a;
|
|
1825
1897
|
this._currentStepTag = tag;
|
|
1826
1898
|
this._stepOrder++;
|
|
1827
1899
|
this._stepResultDependencies = new Set();
|
|
@@ -1880,6 +1952,16 @@ class RecordingContext {
|
|
|
1880
1952
|
const condParts = [];
|
|
1881
1953
|
if (this._scenarioCondition)
|
|
1882
1954
|
condParts.push(`(${this._scenarioCondition})`);
|
|
1955
|
+
if (this._portableCondition) {
|
|
1956
|
+
condParts.push(`(${this._portableCondition})`);
|
|
1957
|
+
const conditionDependencies = [
|
|
1958
|
+
...Array.from(this._portableCondition.matchAll(/\$Step\{([^}]+)\}/g), (match) => match[1]),
|
|
1959
|
+
...Array.from(this._portableCondition.matchAll(/\$Sequence\{main\}\{([^}]+)\}/g), (match) => match[1]),
|
|
1960
|
+
];
|
|
1961
|
+
if (conditionDependencies.length > 0) {
|
|
1962
|
+
step.depends_on = Array.from(new Set([...((_a = step.depends_on) !== null && _a !== void 0 ? _a : []), ...conditionDependencies]));
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1883
1965
|
if (this._pendingConditionFromOverride) {
|
|
1884
1966
|
condParts.push(`(${this._pendingConditionFromOverride})`);
|
|
1885
1967
|
this._pendingConditionFromOverride = null;
|
|
@@ -2212,18 +2294,27 @@ class RecordingContext {
|
|
|
2212
2294
|
};
|
|
2213
2295
|
}
|
|
2214
2296
|
get transform() {
|
|
2297
|
+
const operator = (value) => {
|
|
2298
|
+
const converted = convertToOperatorInput(value, this._inputProxy, this._inputSchemaKeys);
|
|
2299
|
+
return typeof converted === 'string' && converted.startsWith('$') ? converted : null;
|
|
2300
|
+
};
|
|
2301
|
+
const operand = (value) => { var _a; return (_a = operator(value)) !== null && _a !== void 0 ? _a : JSON.stringify(value); };
|
|
2215
2302
|
return {
|
|
2303
|
+
concat: (...parts) => `$Concat([${parts.map(operand).join(', ')}], "")`,
|
|
2216
2304
|
size: (obj) => Object.keys(obj).length,
|
|
2217
2305
|
length: (arr) => arr.length,
|
|
2218
2306
|
parseJson: (str) => JSON.parse(str),
|
|
2219
2307
|
stringify: (obj) => JSON.stringify(obj),
|
|
2220
|
-
upper: (str) => str.toUpperCase(),
|
|
2221
|
-
lower: (str) => str.toLowerCase(),
|
|
2222
|
-
trim: (str) => str.trim(),
|
|
2308
|
+
upper: (str) => operator(str) ? `$Uppercase(${operand(str)})` : str.toUpperCase(),
|
|
2309
|
+
lower: (str) => operator(str) ? `$Lowercase(${operand(str)})` : str.toLowerCase(),
|
|
2310
|
+
trim: (str) => operator(str) ? `$Trim(${operand(str)})` : str.trim(),
|
|
2223
2311
|
split: (str, separator) => str.split(separator),
|
|
2224
2312
|
join: (arr, separator) => arr.join(separator),
|
|
2225
|
-
now: () =>
|
|
2226
|
-
|
|
2313
|
+
now: () => '$Now',
|
|
2314
|
+
uuid: () => '$Uuid',
|
|
2315
|
+
replace: (str, search, replacement) => operator(str) ? `$Replace(${operand(str)}, ${JSON.stringify(search)}, ${JSON.stringify(replacement)})` : str.split(search).join(replacement),
|
|
2316
|
+
substring: (str, start, end) => operator(str) ? `$Substring(${operand(str)}, ${start}, ${end})` : str.substring(start, end),
|
|
2317
|
+
formatDate: (date, format) => operator(date) ? `$Dateformat(${operand(date)}, ${JSON.stringify(format)})` : new Date(date).toISOString(),
|
|
2227
2318
|
};
|
|
2228
2319
|
}
|
|
2229
2320
|
// ==================== DATA REFERENCES ====================
|
|
@@ -2415,6 +2506,19 @@ class FeatureCompiler {
|
|
|
2415
2506
|
constructor(options) {
|
|
2416
2507
|
this.options = options;
|
|
2417
2508
|
}
|
|
2509
|
+
validatePortableControlFlow() {
|
|
2510
|
+
const source = this.options.handler.toString();
|
|
2511
|
+
if (/\bDate\s*\.\s*now\s*\(/.test(source) || /\bMath\s*\.\s*random\s*\(/.test(source)) {
|
|
2512
|
+
throw new FeatureCompilationError(this.options.tag, '(handler)', 'Date.now() and Math.random() run while the Feature is being compiled and would become frozen literals. ' +
|
|
2513
|
+
'Use ctx.transform.now() for the current runtime timestamp. Generate random or unique values through a portable function or reusable action.');
|
|
2514
|
+
}
|
|
2515
|
+
const containsNullishBranch = /\?\?/.test(source) ||
|
|
2516
|
+
/!==\s*null\s*&&[\s\S]{0,300}!==\s*void\s+0/.test(source);
|
|
2517
|
+
if (containsNullishBranch && !this.options.branchOverrides) {
|
|
2518
|
+
throw new FeatureCompilationError(this.options.tag, '(handler)', 'Nullish branching on a step result cannot be inferred safely. Provide branchOverrides for the step output that records this branch, ' +
|
|
2519
|
+
'or use an explicit portable branch primitive. The Feature was not persisted.');
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2418
2522
|
/**
|
|
2419
2523
|
* Compile the feature definition to JSON schema
|
|
2420
2524
|
* Executes the handler with a recording context to capture step definitions
|
|
@@ -2469,6 +2573,7 @@ class FeatureCompiler {
|
|
|
2469
2573
|
*/
|
|
2470
2574
|
async compileAsync() {
|
|
2471
2575
|
var _a, _b, _c, _d, _e;
|
|
2576
|
+
this.validatePortableControlFlow();
|
|
2472
2577
|
const scenarios = this.options.recordScenarios && this.options.recordScenarios.length > 0
|
|
2473
2578
|
? this.options.recordScenarios
|
|
2474
2579
|
: [(_a = this.options.recordInput) !== null && _a !== void 0 ? _a : {}];
|