@ductape/sdk 0.1.152 → 0.1.154

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.
@@ -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;
@@ -1776,6 +1779,8 @@ class RecordingContext {
1776
1779
  /** When set (from recordScenarios), every step we push gets this condition so it only runs when input matches. */
1777
1780
  this._scenarioCondition = null;
1778
1781
  this._recordInput = null;
1782
+ /** Explicit portable control-flow condition inherited by steps in a branch. */
1783
+ this._portableCondition = null;
1779
1784
  this._featureTag = featureTag;
1780
1785
  this._inputSchemaKeys = featureInputSchema ? Object.keys(featureInputSchema) : [];
1781
1786
  this._stepResultOverrides = stepResultOverrides !== null && stepResultOverrides !== void 0 ? stepResultOverrides : {};
@@ -1827,10 +1832,68 @@ class RecordingContext {
1827
1832
  get now() {
1828
1833
  return '$Now';
1829
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
+ }
1830
1892
  /**
1831
1893
  * Record a step definition
1832
1894
  */
1833
1895
  async step(tag, handler, rollback, options) {
1896
+ var _a;
1834
1897
  this._currentStepTag = tag;
1835
1898
  this._stepOrder++;
1836
1899
  this._stepResultDependencies = new Set();
@@ -1889,6 +1952,16 @@ class RecordingContext {
1889
1952
  const condParts = [];
1890
1953
  if (this._scenarioCondition)
1891
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
+ }
1892
1965
  if (this._pendingConditionFromOverride) {
1893
1966
  condParts.push(`(${this._pendingConditionFromOverride})`);
1894
1967
  this._pendingConditionFromOverride = null;
@@ -1960,21 +2033,42 @@ class RecordingContext {
1960
2033
  insert: async (options) => {
1961
2034
  this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, 'insert', {
1962
2035
  database: options.database,
1963
- input: { table: options.table, data: options.data },
2036
+ input: { table: options.table, data: options.data, returning: options.returning },
1964
2037
  });
1965
2038
  return createStepOutputProxy();
1966
2039
  },
1967
2040
  update: async (options) => {
1968
2041
  this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, 'update', {
1969
2042
  database: options.database,
1970
- input: { table: options.table, data: options.data, where: options.where },
2043
+ input: { table: options.table, data: options.data, where: options.where, returning: options.returning },
1971
2044
  });
1972
2045
  return createStepOutputProxy();
1973
2046
  },
1974
2047
  delete: async (options) => {
1975
2048
  this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, 'delete', {
1976
2049
  database: options.database,
1977
- input: { table: options.table, where: options.where },
2050
+ input: { table: options.table, where: options.where, returning: options.returning },
2051
+ });
2052
+ return createStepOutputProxy();
2053
+ },
2054
+ upsert: async (options) => {
2055
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, 'upsert', {
2056
+ database: options.database,
2057
+ input: options,
2058
+ });
2059
+ return createStepOutputProxy();
2060
+ },
2061
+ aggregate: async (options) => {
2062
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, 'aggregate', {
2063
+ database: options.database,
2064
+ input: options,
2065
+ });
2066
+ return createStepOutputProxy();
2067
+ },
2068
+ raw: async (options) => {
2069
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, 'raw', {
2070
+ database: options.database,
2071
+ input: options,
1978
2072
  });
1979
2073
  return createStepOutputProxy();
1980
2074
  },
@@ -2221,18 +2315,27 @@ class RecordingContext {
2221
2315
  };
2222
2316
  }
2223
2317
  get transform() {
2318
+ const operator = (value) => {
2319
+ const converted = convertToOperatorInput(value, this._inputProxy, this._inputSchemaKeys);
2320
+ return typeof converted === 'string' && converted.startsWith('$') ? converted : null;
2321
+ };
2322
+ const operand = (value) => { var _a; return (_a = operator(value)) !== null && _a !== void 0 ? _a : JSON.stringify(value); };
2224
2323
  return {
2324
+ concat: (...parts) => `$Concat([${parts.map(operand).join(', ')}], "")`,
2225
2325
  size: (obj) => Object.keys(obj).length,
2226
2326
  length: (arr) => arr.length,
2227
2327
  parseJson: (str) => JSON.parse(str),
2228
2328
  stringify: (obj) => JSON.stringify(obj),
2229
- upper: (str) => str.toUpperCase(),
2230
- lower: (str) => str.toLowerCase(),
2231
- trim: (str) => str.trim(),
2329
+ upper: (str) => operator(str) ? `$Uppercase(${operand(str)})` : str.toUpperCase(),
2330
+ lower: (str) => operator(str) ? `$Lowercase(${operand(str)})` : str.toLowerCase(),
2331
+ trim: (str) => operator(str) ? `$Trim(${operand(str)})` : str.trim(),
2232
2332
  split: (str, separator) => str.split(separator),
2233
2333
  join: (arr, separator) => arr.join(separator),
2234
- now: () => Date.now(),
2235
- formatDate: (date, format) => new Date(date).toISOString(),
2334
+ now: () => '$Now',
2335
+ uuid: () => '$Uuid',
2336
+ replace: (str, search, replacement) => operator(str) ? `$Replace(${operand(str)}, ${JSON.stringify(search)}, ${JSON.stringify(replacement)})` : str.split(search).join(replacement),
2337
+ substring: (str, start, end) => operator(str) ? `$Substring(${operand(str)}, ${start}, ${end})` : str.substring(start, end),
2338
+ formatDate: (date, format) => operator(date) ? `$Dateformat(${operand(date)}, ${JSON.stringify(format)})` : new Date(date).toISOString(),
2236
2339
  };
2237
2340
  }
2238
2341
  // ==================== DATA REFERENCES ====================
@@ -2424,6 +2527,19 @@ class FeatureCompiler {
2424
2527
  constructor(options) {
2425
2528
  this.options = options;
2426
2529
  }
2530
+ validatePortableControlFlow() {
2531
+ const source = this.options.handler.toString();
2532
+ if (/\bDate\s*\.\s*now\s*\(/.test(source) || /\bMath\s*\.\s*random\s*\(/.test(source)) {
2533
+ throw new FeatureCompilationError(this.options.tag, '(handler)', 'Date.now() and Math.random() run while the Feature is being compiled and would become frozen literals. ' +
2534
+ 'Use ctx.transform.now() for the current runtime timestamp. Generate random or unique values through a portable function or reusable action.');
2535
+ }
2536
+ const containsNullishBranch = /\?\?/.test(source) ||
2537
+ /!==\s*null\s*&&[\s\S]{0,300}!==\s*void\s+0/.test(source);
2538
+ if (containsNullishBranch && !this.options.branchOverrides) {
2539
+ 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, ' +
2540
+ 'or use an explicit portable branch primitive. The Feature was not persisted.');
2541
+ }
2542
+ }
2427
2543
  /**
2428
2544
  * Compile the feature definition to JSON schema
2429
2545
  * Executes the handler with a recording context to capture step definitions
@@ -2478,6 +2594,7 @@ class FeatureCompiler {
2478
2594
  */
2479
2595
  async compileAsync() {
2480
2596
  var _a, _b, _c, _d, _e;
2597
+ this.validatePortableControlFlow();
2481
2598
  const scenarios = this.options.recordScenarios && this.options.recordScenarios.length > 0
2482
2599
  ? this.options.recordScenarios
2483
2600
  : [(_a = this.options.recordInput) !== null && _a !== void 0 ? _a : {}];