@infitx/match 1.3.2 → 1.4.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0](https://github.com/infitx-org/release-cd/compare/match-v1.3.2...match-v1.4.0) (2026-02-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * add $ref support for dynamic fact references in match rules ([#135](https://github.com/infitx-org/release-cd/issues/135)) ([032b207](https://github.com/infitx-org/release-cd/commit/032b20734cbe559b25126c25e5d7130e1a3cf7b3))
9
+
3
10
  ## [1.3.2](https://github.com/infitx-org/release-cd/compare/match-v1.3.1...match-v1.3.2) (2026-02-09)
4
11
 
5
12
 
package/README.md CHANGED
@@ -22,6 +22,74 @@ match({ a: 1 }, { a: 2 }); // false
22
22
  match({ a: 1, b: 2 }, { a: 1 }); // true
23
23
  ```
24
24
 
25
+ ## Referencing Fact Values with `$ref`
26
+
27
+ The `$ref` feature allows rules to reference values from other parts of the fact using JSON Pointer syntax. This is useful for dynamic comparisons where the expected value comes from the fact itself.
28
+
29
+ ```javascript
30
+ // Reference another property in the fact
31
+ match(
32
+ { expectedStatus: 'active', order: { status: 'active' } },
33
+ { order: { status: { $ref: '#/expectedStatus' } } }
34
+ ); // true - order.status matches expectedStatus
35
+
36
+ // Use $ref in min/max range conditions
37
+ match(
38
+ { offer: { dateCreated: '2024-01-01' }, order: { dateCreated: '2024-02-01' } },
39
+ { order: { dateCreated: { min: { $ref: '#/offer/dateCreated' } } } }
40
+ ); // true - order date is after offer date
41
+
42
+ // Multiple $ref in same rule
43
+ match(
44
+ { source: 'Alice', destination: 'Bob', transfer: { from: 'Alice', to: 'Bob' } },
45
+ { transfer: { from: { $ref: '#/source' }, to: { $ref: '#/destination' } } }
46
+ ); // true - transfer from/to matches source/destination
47
+
48
+ // $ref with nested paths
49
+ match(
50
+ { config: { pricing: { minPrice: 100 } }, order: { price: 150 } },
51
+ { order: { price: { min: { $ref: '#/config/pricing/minPrice' } } } }
52
+ ); // true - order price is above configured minimum
53
+
54
+ // Missing properties resolve to undefined
55
+ match(
56
+ { order: { dateCreated: '2024-02-01' } },
57
+ { order: { dateCreated: { min: { $ref: '#/offer/dateCreated' } } } }
58
+ ); // true - missing reference is treated as no constraint
59
+
60
+ // $ref with type coercion
61
+ match(
62
+ { targetAmount: '500', order: { amount: 500 } },
63
+ { order: { amount: { $ref: '#/targetAmount' } } }
64
+ ); // true - string '500' is coerced to number 500
65
+
66
+ // $ref with arrays (any-of semantics apply)
67
+ match(
68
+ { allowedTag: 'vip', order: { tags: ['vip', 'premium'] } },
69
+ { order: { tags: { $ref: '#/allowedTag' } } }
70
+ ); // true - at least one tag matches the reference
71
+ ```
72
+
73
+ ### JSON Pointer Syntax
74
+
75
+ The `$ref` value uses JSON Pointer syntax (RFC 6901):
76
+ - Always starts with `#/` to indicate the root of the fact object
77
+ - Properties are separated by `/`
78
+ - Examples:
79
+ - `#/offer/dateCreated` → `fact.offer.dateCreated`
80
+ - `#/config/pricing/minPrice` → `fact.config.pricing.minPrice`
81
+ - `#/expectedStatus` → `fact.expectedStatus`
82
+
83
+ ### When to Use `$ref`
84
+
85
+ Use `$ref` when you need to:
86
+ - Compare related values within the same fact
87
+ - Implement dynamic constraints based on other properties
88
+ - Reference configuration values from the fact
89
+ - Create rules that adapt to the data being matched
90
+
91
+ ```
92
+
25
93
  ## Matching Against Nested Structures
26
94
 
27
95
  The match function recursively compares nested objects:
package/index.js CHANGED
@@ -1,6 +1,18 @@
1
1
  const isMatchWith = require('lodash/isMatchWith');
2
2
  const isPlainObject = require('lodash/isPlainObject');
3
3
 
4
+ // Resolve JSON Pointer path from fact (e.g., "#/offer/dateCreated")
5
+ function resolveRef(refPath, factValue) {
6
+ if (typeof refPath !== 'string' || !refPath.startsWith('#/')) return undefined;
7
+ const path = refPath.slice(2).split('/');
8
+ let value = factValue;
9
+ for (const key of path) {
10
+ if (value == null) return undefined;
11
+ value = value[key];
12
+ }
13
+ return value;
14
+ }
15
+
4
16
  // Round a date to the start of a time unit
5
17
  function roundToUnit(date, unit) {
6
18
  const d = new Date(date);
@@ -72,7 +84,6 @@ function parseTimeInterval(str, referenceTime) {
72
84
  };
73
85
 
74
86
  const offset = multiplier * value * unitMultipliers[unit];
75
- console.log({ referenceTime, offset });
76
87
  const resultTime = new Date(referenceTime + offset);
77
88
 
78
89
  // Apply rounding if specified
@@ -83,15 +94,20 @@ function parseTimeInterval(str, referenceTime) {
83
94
  return resultTime;
84
95
  }
85
96
 
86
- const match = (referenceTime) => (value, condition) => {
97
+ const match = (referenceTime, rootFact) => (value, condition) => {
98
+ // Resolve $ref if condition is a reference object
99
+ if (isPlainObject(condition) && '$ref' in condition && Object.keys(condition).length === 1) {
100
+ condition = resolveRef(condition.$ref, rootFact);
101
+ }
102
+
87
103
  if (value == null && condition == null) {
88
104
  return true
89
105
  } else if (value === condition) {
90
106
  return true;
91
107
  } else if (Array.isArray(condition) || Array.isArray(value)) {
92
108
  return Array.isArray(value)
93
- ? value.some(v => module.exports(v, condition, referenceTime))
94
- : condition.some(v => module.exports(value, v, referenceTime));
109
+ ? value.some(v => module.exports(v, condition, referenceTime, rootFact))
110
+ : condition.some(v => module.exports(value, v, referenceTime, rootFact));
95
111
  } else if (value == null || condition == null) {
96
112
  return false;
97
113
  } else if (condition instanceof RegExp) {
@@ -113,7 +129,15 @@ const match = (referenceTime) => (value, condition) => {
113
129
  return condition(value);
114
130
  case 'object': {
115
131
  let { min, max, not } = condition;
116
- if (not != null) return !module.exports(value, not, referenceTime);
132
+ if (not != null) return !module.exports(value, not, referenceTime, rootFact);
133
+
134
+ // Resolve $ref in min and max
135
+ if (isPlainObject(min) && min.$ref) {
136
+ min = resolveRef(min.$ref, rootFact);
137
+ }
138
+ if (isPlainObject(max) && max.$ref) {
139
+ max = resolveRef(max.$ref, rootFact);
140
+ }
117
141
 
118
142
  // Parse Grafana-style time intervals for min and max
119
143
  if (typeof min === 'string') {
@@ -125,7 +149,6 @@ const match = (referenceTime) => (value, condition) => {
125
149
  if (parsed) max = parsed;
126
150
  }
127
151
 
128
- console.log({ min, max, value });
129
152
  if (value instanceof Date) {
130
153
  value = value.getTime();
131
154
  if (!Number.isFinite(value)) return false;
@@ -144,24 +167,29 @@ const match = (referenceTime) => (value, condition) => {
144
167
  if (max != null && (value > max || value === Infinity || max === -Infinity))
145
168
  return false;
146
169
  if (min != null || max != null) return true;
147
- if (typeof value === 'object' && value && condition) return module.exports(value, condition, referenceTime);
170
+ // If condition only contains min/max/not (which have been handled above),
171
+ // and all resolved to undefined/null, return true (no constraints to enforce)
172
+ const conditionKeys = Object.keys(condition);
173
+ if (conditionKeys.every(k => ['min', 'max', 'not'].includes(k))) return true;
174
+ if (typeof value === 'object' && value && condition) return module.exports(value, condition, referenceTime, rootFact);
148
175
  }
149
176
  }
150
177
  }
151
178
 
152
- module.exports = function (factValue, ruleValue, referenceTime = Date.now()) {
179
+ module.exports = function (factValue, ruleValue, referenceTime = Date.now(), rootFact = undefined) {
180
+ if (rootFact === undefined) rootFact = factValue; // Store the root fact for $ref resolution only on first call
153
181
  if (factValue === ruleValue) return true;
154
- if (ruleValue && typeof ruleValue === 'object' && 'not' in ruleValue && Object.keys(ruleValue).length === 1) return !module.exports(factValue, ruleValue.not, referenceTime);
182
+ if (ruleValue && typeof ruleValue === 'object' && 'not' in ruleValue && Object.keys(ruleValue).length === 1) return !module.exports(factValue, ruleValue.not, referenceTime, rootFact);
155
183
  if (
156
184
  Array.isArray(factValue) ||
157
185
  Array.isArray(ruleValue) ||
158
186
  !isPlainObject(factValue) ||
159
187
  !isPlainObject(ruleValue)
160
188
  )
161
- return match(referenceTime)(factValue, ruleValue);
189
+ return match(referenceTime, rootFact)(factValue, ruleValue);
162
190
  if (factValue && ruleValue && typeof factValue === 'object' && typeof ruleValue === 'object') {
163
191
  const nullFilter = Object.entries(ruleValue).filter(([, value]) => value == null || Array.isArray(value));
164
192
  if (nullFilter.length > 0) factValue = { ...Object.fromEntries(nullFilter), ...factValue };
165
193
  };
166
- return isMatchWith(factValue, ruleValue, match(referenceTime));
194
+ return isMatchWith(factValue, ruleValue, match(referenceTime, rootFact));
167
195
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infitx/match",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Object pattern matching utility",
5
5
  "main": "index.js",
6
6
  "scripts": {