@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 +7 -0
- package/README.md +68 -0
- package/index.js +39 -11
- package/package.json +1 -1
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
|
-
|
|
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
|
};
|