@traffical/core 0.1.2
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/dedup/decision-dedup.d.ts +74 -0
- package/dist/dedup/decision-dedup.d.ts.map +1 -0
- package/dist/dedup/decision-dedup.js +132 -0
- package/dist/dedup/decision-dedup.js.map +1 -0
- package/dist/dedup/index.d.ts +5 -0
- package/dist/dedup/index.d.ts.map +1 -0
- package/dist/dedup/index.js +5 -0
- package/dist/dedup/index.js.map +1 -0
- package/dist/edge/client.d.ts +109 -0
- package/dist/edge/client.d.ts.map +1 -0
- package/dist/edge/client.js +154 -0
- package/dist/edge/client.js.map +1 -0
- package/dist/edge/index.d.ts +7 -0
- package/dist/edge/index.d.ts.map +1 -0
- package/dist/edge/index.js +7 -0
- package/dist/edge/index.js.map +1 -0
- package/dist/hashing/bucket.d.ts +56 -0
- package/dist/hashing/bucket.d.ts.map +1 -0
- package/dist/hashing/bucket.js +89 -0
- package/dist/hashing/bucket.js.map +1 -0
- package/dist/hashing/fnv1a.d.ts +17 -0
- package/dist/hashing/fnv1a.d.ts.map +1 -0
- package/dist/hashing/fnv1a.js +27 -0
- package/dist/hashing/fnv1a.js.map +1 -0
- package/dist/hashing/index.d.ts +8 -0
- package/dist/hashing/index.d.ts.map +1 -0
- package/dist/hashing/index.js +8 -0
- package/dist/hashing/index.js.map +1 -0
- package/dist/ids/index.d.ts +83 -0
- package/dist/ids/index.d.ts.map +1 -0
- package/dist/ids/index.js +165 -0
- package/dist/ids/index.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/resolution/conditions.d.ts +81 -0
- package/dist/resolution/conditions.d.ts.map +1 -0
- package/dist/resolution/conditions.js +197 -0
- package/dist/resolution/conditions.js.map +1 -0
- package/dist/resolution/engine.d.ts +54 -0
- package/dist/resolution/engine.d.ts.map +1 -0
- package/dist/resolution/engine.js +382 -0
- package/dist/resolution/engine.js.map +1 -0
- package/dist/resolution/index.d.ts +8 -0
- package/dist/resolution/index.d.ts.map +1 -0
- package/dist/resolution/index.js +10 -0
- package/dist/resolution/index.js.map +1 -0
- package/dist/types/index.d.ts +440 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +51 -0
- package/src/dedup/decision-dedup.ts +175 -0
- package/src/dedup/index.ts +6 -0
- package/src/edge/client.ts +256 -0
- package/src/edge/index.ts +16 -0
- package/src/hashing/bucket.ts +115 -0
- package/src/hashing/fnv1a.test.ts +87 -0
- package/src/hashing/fnv1a.ts +31 -0
- package/src/hashing/index.ts +15 -0
- package/src/ids/index.ts +221 -0
- package/src/index.ts +136 -0
- package/src/resolution/conditions.ts +253 -0
- package/src/resolution/engine.test.ts +242 -0
- package/src/resolution/engine.ts +480 -0
- package/src/resolution/index.ts +32 -0
- package/src/types/index.ts +508 -0
package/src/ids/index.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID Generation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent ID generation with type prefixes for all entities and events.
|
|
5
|
+
*
|
|
6
|
+
* Entity IDs: 8-character NanoID with prefix (e.g., "proj_hVF1cCoC")
|
|
7
|
+
* - Compact and URL-friendly
|
|
8
|
+
* - 64^8 = 281 trillion combinations
|
|
9
|
+
* - With DB constraints, collisions are handled via retry
|
|
10
|
+
*
|
|
11
|
+
* Event IDs: ULID with prefix (e.g., "dec_01JHFK1WWMMG7M0XPEBTYXZEBW")
|
|
12
|
+
* - Lexicographically sortable (time-ordered)
|
|
13
|
+
* - Contains millisecond timestamp for analytics
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { customAlphabet } from "nanoid";
|
|
17
|
+
import { ulid } from "ulid";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// NanoID Configuration
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* URL-safe alphabet for NanoID (64 characters).
|
|
25
|
+
* Includes: 0-9, A-Z, a-z (no special chars to avoid URL encoding issues)
|
|
26
|
+
*/
|
|
27
|
+
const NANOID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default length for entity IDs (without prefix).
|
|
31
|
+
* 64^8 = 281,474,976,710,656 (~281 trillion) combinations.
|
|
32
|
+
*/
|
|
33
|
+
const ENTITY_ID_LENGTH = 8;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* NanoID generator with custom alphabet.
|
|
37
|
+
*/
|
|
38
|
+
const nanoid = customAlphabet(NANOID_ALPHABET, ENTITY_ID_LENGTH);
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// ID Prefixes
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Entity ID prefixes for each entity type.
|
|
46
|
+
*/
|
|
47
|
+
export type EntityIdPrefix =
|
|
48
|
+
| "org" // Organization
|
|
49
|
+
| "proj" // Project
|
|
50
|
+
| "env" // Environment
|
|
51
|
+
| "ns" // Namespace
|
|
52
|
+
| "lay" // Layer
|
|
53
|
+
| "pol" // Policy
|
|
54
|
+
| "alloc" // Allocation
|
|
55
|
+
| "param" // Parameter
|
|
56
|
+
| "dom" // DOM Binding
|
|
57
|
+
| "ovr" // Environment Override
|
|
58
|
+
| "ak"; // API Key
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Event ID prefixes for each event type.
|
|
62
|
+
*/
|
|
63
|
+
export type EventIdPrefix = "dec" | "exp" | "trk";
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Generic ID Generation
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generates a prefixed 8-char NanoID for the specified entity type.
|
|
71
|
+
*
|
|
72
|
+
* @param prefix - The entity type prefix
|
|
73
|
+
* @returns A prefixed NanoID string (e.g., "proj_hVF1cCoC")
|
|
74
|
+
*/
|
|
75
|
+
export function generateEntityId(prefix: EntityIdPrefix): string {
|
|
76
|
+
return `${prefix}_${nanoid()}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generates a prefixed ULID for the specified event type.
|
|
81
|
+
* Events use ULID for time-sortability in analytics.
|
|
82
|
+
*
|
|
83
|
+
* @param prefix - The event type prefix
|
|
84
|
+
* @returns A prefixed ULID string (e.g., "dec_01JHFK1WWMMG7M0XPEBTYXZEBW")
|
|
85
|
+
*/
|
|
86
|
+
export function generateEventId(prefix: EventIdPrefix): string {
|
|
87
|
+
return `${prefix}_${ulid()}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generates a plain 8-char NanoID without prefix.
|
|
92
|
+
* Used for internal IDs that don't need type identification.
|
|
93
|
+
*/
|
|
94
|
+
export function generateShortId(): string {
|
|
95
|
+
return nanoid();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Entity ID Convenience Functions
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
/** Generates an Organization ID with "org_" prefix */
|
|
103
|
+
export function generateOrgId(): string {
|
|
104
|
+
return generateEntityId("org");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Generates a Project ID with "proj_" prefix */
|
|
108
|
+
export function generateProjectId(): string {
|
|
109
|
+
return generateEntityId("proj");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Generates an Environment ID with "env_" prefix */
|
|
113
|
+
export function generateEnvironmentId(): string {
|
|
114
|
+
return generateEntityId("env");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Generates a Namespace ID with "ns_" prefix */
|
|
118
|
+
export function generateNamespaceId(): string {
|
|
119
|
+
return generateEntityId("ns");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Generates a Layer ID with "lay_" prefix */
|
|
123
|
+
export function generateLayerId(): string {
|
|
124
|
+
return generateEntityId("lay");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Generates a Policy ID with "pol_" prefix */
|
|
128
|
+
export function generatePolicyId(): string {
|
|
129
|
+
return generateEntityId("pol");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Generates an Allocation ID with "alloc_" prefix */
|
|
133
|
+
export function generateAllocationId(): string {
|
|
134
|
+
return generateEntityId("alloc");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Generates a Parameter ID with "param_" prefix */
|
|
138
|
+
export function generateParameterId(): string {
|
|
139
|
+
return generateEntityId("param");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Generates a DOM Binding ID with "dom_" prefix */
|
|
143
|
+
export function generateDomBindingId(): string {
|
|
144
|
+
return generateEntityId("dom");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Generates an Environment Override ID with "ovr_" prefix */
|
|
148
|
+
export function generateOverrideId(): string {
|
|
149
|
+
return generateEntityId("ovr");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Generates an API Key ID with "ak_" prefix */
|
|
153
|
+
export function generateApiKeyId(): string {
|
|
154
|
+
return generateEntityId("ak");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Event ID Convenience Functions (Keep ULID for time-sortability)
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
/** Generates a Decision event ID with "dec_" prefix (ULID) */
|
|
162
|
+
export function generateDecisionId(): string {
|
|
163
|
+
return generateEventId("dec");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Generates an Exposure event ID with "exp_" prefix (ULID) */
|
|
167
|
+
export function generateExposureId(): string {
|
|
168
|
+
return generateEventId("exp");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Generates a Track event ID with "trk_" prefix (ULID) */
|
|
172
|
+
export function generateTrackEventId(): string {
|
|
173
|
+
return generateEventId("trk");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Utilities
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extracts the timestamp from a ULID-based ID.
|
|
182
|
+
* Only works for event IDs (ULID format).
|
|
183
|
+
*
|
|
184
|
+
* @param id - A prefixed ULID (e.g., "dec_01JHFK1WWMMG7M0XPEBTYXZEBW")
|
|
185
|
+
* @returns The timestamp as a Date, or null if invalid
|
|
186
|
+
*/
|
|
187
|
+
export function getIdTimestamp(id: string): Date | null {
|
|
188
|
+
// Extract the ULID part (after the prefix and underscore)
|
|
189
|
+
const parts = id.split("_");
|
|
190
|
+
if (parts.length < 2) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const ulidPart = parts[1];
|
|
195
|
+
if (!ulidPart || ulidPart.length !== 26) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ULID timestamp is encoded in the first 10 characters (Crockford's Base32)
|
|
200
|
+
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
201
|
+
const TIME_LEN = 10;
|
|
202
|
+
|
|
203
|
+
let time = 0;
|
|
204
|
+
for (let i = 0; i < TIME_LEN; i++) {
|
|
205
|
+
const char = ulidPart.charAt(i).toUpperCase();
|
|
206
|
+
const index = ENCODING.indexOf(char);
|
|
207
|
+
if (index === -1) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
time = time * 32 + index;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return new Date(time);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @deprecated Use getIdTimestamp instead
|
|
218
|
+
*/
|
|
219
|
+
export function getEventIdTimestamp(eventId: string): Date | null {
|
|
220
|
+
return getIdTimestamp(eventId);
|
|
221
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @traffical/core
|
|
3
|
+
*
|
|
4
|
+
* Pure TypeScript core for Traffical SDK.
|
|
5
|
+
* This package performs no I/O and can be used in any JavaScript environment.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Deterministic parameter resolution
|
|
9
|
+
* - FNV-1a hashing for bucket assignment
|
|
10
|
+
* - Condition evaluation for targeting
|
|
11
|
+
* - Defaults-based graceful degradation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Types
|
|
15
|
+
export type {
|
|
16
|
+
// Base types
|
|
17
|
+
Timestamp,
|
|
18
|
+
Id,
|
|
19
|
+
ParameterType,
|
|
20
|
+
ParameterValue,
|
|
21
|
+
Context,
|
|
22
|
+
// Bundle types
|
|
23
|
+
ConfigBundle,
|
|
24
|
+
BundleHashingConfig,
|
|
25
|
+
BundleParameter,
|
|
26
|
+
BundleDOMBinding,
|
|
27
|
+
BundleLayer,
|
|
28
|
+
BundlePolicy,
|
|
29
|
+
BundleAllocation,
|
|
30
|
+
BundleCondition,
|
|
31
|
+
PolicyState,
|
|
32
|
+
PolicyKind,
|
|
33
|
+
ConditionOperator,
|
|
34
|
+
// Per-entity types
|
|
35
|
+
EntityConfig,
|
|
36
|
+
EntityWeights,
|
|
37
|
+
BundleEntityPolicyState,
|
|
38
|
+
// SDK types
|
|
39
|
+
ParameterDefaults,
|
|
40
|
+
DecisionResult,
|
|
41
|
+
DecisionMetadata,
|
|
42
|
+
LayerResolution,
|
|
43
|
+
// Event types
|
|
44
|
+
BaseEventFields,
|
|
45
|
+
ExposureEvent,
|
|
46
|
+
TrackEvent,
|
|
47
|
+
TrackAttribution,
|
|
48
|
+
DecisionEvent,
|
|
49
|
+
TrackableEvent,
|
|
50
|
+
// Client types
|
|
51
|
+
TrafficalClientOptions,
|
|
52
|
+
GetParamsOptions,
|
|
53
|
+
DecideOptions,
|
|
54
|
+
TrackOptions,
|
|
55
|
+
} from "./types/index.js";
|
|
56
|
+
|
|
57
|
+
// Hashing
|
|
58
|
+
export {
|
|
59
|
+
fnv1a,
|
|
60
|
+
computeBucket,
|
|
61
|
+
isInBucketRange,
|
|
62
|
+
findMatchingAllocation,
|
|
63
|
+
percentageToBucketRange,
|
|
64
|
+
createBucketRanges,
|
|
65
|
+
} from "./hashing/index.js";
|
|
66
|
+
|
|
67
|
+
// Resolution
|
|
68
|
+
export {
|
|
69
|
+
resolveParameters,
|
|
70
|
+
decide,
|
|
71
|
+
getUnitKeyValue,
|
|
72
|
+
evaluateCondition,
|
|
73
|
+
evaluateConditions,
|
|
74
|
+
// Condition builders
|
|
75
|
+
eq,
|
|
76
|
+
neq,
|
|
77
|
+
inValues,
|
|
78
|
+
notIn,
|
|
79
|
+
gt,
|
|
80
|
+
gte,
|
|
81
|
+
lt,
|
|
82
|
+
lte,
|
|
83
|
+
contains,
|
|
84
|
+
startsWith,
|
|
85
|
+
endsWith,
|
|
86
|
+
regex,
|
|
87
|
+
exists,
|
|
88
|
+
notExists,
|
|
89
|
+
} from "./resolution/index.js";
|
|
90
|
+
|
|
91
|
+
// Deduplication
|
|
92
|
+
export {
|
|
93
|
+
DecisionDeduplicator,
|
|
94
|
+
type DecisionDeduplicatorOptions,
|
|
95
|
+
} from "./dedup/index.js";
|
|
96
|
+
|
|
97
|
+
// ID Generation
|
|
98
|
+
export {
|
|
99
|
+
// Event IDs (ULID - time-sortable)
|
|
100
|
+
generateEventId,
|
|
101
|
+
generateDecisionId,
|
|
102
|
+
generateExposureId,
|
|
103
|
+
generateTrackEventId,
|
|
104
|
+
// Entity IDs (8-char NanoID)
|
|
105
|
+
generateEntityId,
|
|
106
|
+
generateShortId,
|
|
107
|
+
generateOrgId,
|
|
108
|
+
generateProjectId,
|
|
109
|
+
generateEnvironmentId,
|
|
110
|
+
generateNamespaceId,
|
|
111
|
+
generateLayerId,
|
|
112
|
+
generatePolicyId,
|
|
113
|
+
generateAllocationId,
|
|
114
|
+
generateParameterId,
|
|
115
|
+
generateDomBindingId,
|
|
116
|
+
generateOverrideId,
|
|
117
|
+
generateApiKeyId,
|
|
118
|
+
// Utilities
|
|
119
|
+
getIdTimestamp,
|
|
120
|
+
getEventIdTimestamp, // deprecated
|
|
121
|
+
// Types
|
|
122
|
+
type EventIdPrefix,
|
|
123
|
+
type EntityIdPrefix,
|
|
124
|
+
} from "./ids/index.js";
|
|
125
|
+
|
|
126
|
+
// Edge Client (for per-entity policies)
|
|
127
|
+
export {
|
|
128
|
+
EdgeClient,
|
|
129
|
+
createEdgeDecideRequest,
|
|
130
|
+
type EdgeClientConfig,
|
|
131
|
+
type EdgeDecideRequest,
|
|
132
|
+
type EdgeDecideResponse,
|
|
133
|
+
type EdgeBatchDecideRequest,
|
|
134
|
+
type EdgeBatchDecideResponse,
|
|
135
|
+
} from "./edge/index.js";
|
|
136
|
+
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condition Evaluation
|
|
3
|
+
*
|
|
4
|
+
* Evaluates context predicates to determine policy eligibility.
|
|
5
|
+
* Conditions are AND-ed together: all must match for a policy to apply.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Context, BundleCondition } from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Evaluates a single condition against a context.
|
|
12
|
+
*
|
|
13
|
+
* @param condition - The condition to evaluate
|
|
14
|
+
* @param context - The context to evaluate against
|
|
15
|
+
* @returns True if the condition matches
|
|
16
|
+
*/
|
|
17
|
+
export function evaluateCondition(
|
|
18
|
+
condition: BundleCondition,
|
|
19
|
+
context: Context
|
|
20
|
+
): boolean {
|
|
21
|
+
const { field, op, value, values } = condition;
|
|
22
|
+
|
|
23
|
+
// Get the context value using dot notation
|
|
24
|
+
const contextValue = getNestedValue(context, field);
|
|
25
|
+
|
|
26
|
+
switch (op) {
|
|
27
|
+
case "eq":
|
|
28
|
+
return contextValue === value;
|
|
29
|
+
|
|
30
|
+
case "neq":
|
|
31
|
+
return contextValue !== value;
|
|
32
|
+
|
|
33
|
+
case "in":
|
|
34
|
+
if (!Array.isArray(values)) return false;
|
|
35
|
+
return values.includes(contextValue);
|
|
36
|
+
|
|
37
|
+
case "nin":
|
|
38
|
+
if (!Array.isArray(values)) return true;
|
|
39
|
+
return !values.includes(contextValue);
|
|
40
|
+
|
|
41
|
+
case "gt":
|
|
42
|
+
return (
|
|
43
|
+
typeof contextValue === "number" && contextValue > (value as number)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
case "gte":
|
|
47
|
+
return (
|
|
48
|
+
typeof contextValue === "number" && contextValue >= (value as number)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
case "lt":
|
|
52
|
+
return (
|
|
53
|
+
typeof contextValue === "number" && contextValue < (value as number)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
case "lte":
|
|
57
|
+
return (
|
|
58
|
+
typeof contextValue === "number" && contextValue <= (value as number)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
case "contains":
|
|
62
|
+
return (
|
|
63
|
+
typeof contextValue === "string" &&
|
|
64
|
+
typeof value === "string" &&
|
|
65
|
+
contextValue.includes(value)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
case "startsWith":
|
|
69
|
+
return (
|
|
70
|
+
typeof contextValue === "string" &&
|
|
71
|
+
typeof value === "string" &&
|
|
72
|
+
contextValue.startsWith(value)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
case "endsWith":
|
|
76
|
+
return (
|
|
77
|
+
typeof contextValue === "string" &&
|
|
78
|
+
typeof value === "string" &&
|
|
79
|
+
contextValue.endsWith(value)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
case "regex":
|
|
83
|
+
if (typeof contextValue !== "string" || typeof value !== "string") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const regex = new RegExp(value);
|
|
88
|
+
return regex.test(contextValue);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case "exists":
|
|
94
|
+
return contextValue !== undefined && contextValue !== null;
|
|
95
|
+
|
|
96
|
+
case "notExists":
|
|
97
|
+
return contextValue === undefined || contextValue === null;
|
|
98
|
+
|
|
99
|
+
default:
|
|
100
|
+
// Unknown operator, fail safe by not matching
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Evaluates all conditions against a context.
|
|
107
|
+
* All conditions must match (AND logic).
|
|
108
|
+
*
|
|
109
|
+
* @param conditions - Array of conditions
|
|
110
|
+
* @param context - The context to evaluate against
|
|
111
|
+
* @returns True if all conditions match (or if there are no conditions)
|
|
112
|
+
*/
|
|
113
|
+
export function evaluateConditions(
|
|
114
|
+
conditions: BundleCondition[],
|
|
115
|
+
context: Context
|
|
116
|
+
): boolean {
|
|
117
|
+
// Empty conditions = always match
|
|
118
|
+
if (conditions.length === 0) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// All conditions must match (AND)
|
|
123
|
+
return conditions.every((condition) => evaluateCondition(condition, context));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gets a nested value from an object using dot notation.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* getNestedValue({ user: { name: "Alice" } }, "user.name") // "Alice"
|
|
131
|
+
* getNestedValue({ tags: ["a", "b"] }, "tags.0") // "a"
|
|
132
|
+
*/
|
|
133
|
+
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
134
|
+
const parts = path.split(".");
|
|
135
|
+
let current: unknown = obj;
|
|
136
|
+
|
|
137
|
+
for (const part of parts) {
|
|
138
|
+
if (current === null || current === undefined) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof current === "object") {
|
|
143
|
+
current = (current as Record<string, unknown>)[part];
|
|
144
|
+
} else {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return current;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// Condition Builder Helpers
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates an equality condition.
|
|
158
|
+
*/
|
|
159
|
+
export function eq(field: string, value: unknown): BundleCondition {
|
|
160
|
+
return { field, op: "eq", value };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a not-equal condition.
|
|
165
|
+
*/
|
|
166
|
+
export function neq(field: string, value: unknown): BundleCondition {
|
|
167
|
+
return { field, op: "neq", value };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Creates an "in" condition.
|
|
172
|
+
*/
|
|
173
|
+
export function inValues(field: string, values: unknown[]): BundleCondition {
|
|
174
|
+
return { field, op: "in", values };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates a "not in" condition.
|
|
179
|
+
*/
|
|
180
|
+
export function notIn(field: string, values: unknown[]): BundleCondition {
|
|
181
|
+
return { field, op: "nin", values };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Creates a greater-than condition.
|
|
186
|
+
*/
|
|
187
|
+
export function gt(field: string, value: number): BundleCondition {
|
|
188
|
+
return { field, op: "gt", value };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Creates a greater-than-or-equal condition.
|
|
193
|
+
*/
|
|
194
|
+
export function gte(field: string, value: number): BundleCondition {
|
|
195
|
+
return { field, op: "gte", value };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Creates a less-than condition.
|
|
200
|
+
*/
|
|
201
|
+
export function lt(field: string, value: number): BundleCondition {
|
|
202
|
+
return { field, op: "lt", value };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Creates a less-than-or-equal condition.
|
|
207
|
+
*/
|
|
208
|
+
export function lte(field: string, value: number): BundleCondition {
|
|
209
|
+
return { field, op: "lte", value };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Creates a string contains condition.
|
|
214
|
+
*/
|
|
215
|
+
export function contains(field: string, value: string): BundleCondition {
|
|
216
|
+
return { field, op: "contains", value };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Creates a string starts-with condition.
|
|
221
|
+
*/
|
|
222
|
+
export function startsWith(field: string, value: string): BundleCondition {
|
|
223
|
+
return { field, op: "startsWith", value };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Creates a string ends-with condition.
|
|
228
|
+
*/
|
|
229
|
+
export function endsWith(field: string, value: string): BundleCondition {
|
|
230
|
+
return { field, op: "endsWith", value };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates a regex match condition.
|
|
235
|
+
*/
|
|
236
|
+
export function regex(field: string, pattern: string): BundleCondition {
|
|
237
|
+
return { field, op: "regex", value: pattern };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Creates an exists condition.
|
|
242
|
+
*/
|
|
243
|
+
export function exists(field: string): BundleCondition {
|
|
244
|
+
return { field, op: "exists" };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Creates a not-exists condition.
|
|
249
|
+
*/
|
|
250
|
+
export function notExists(field: string): BundleCondition {
|
|
251
|
+
return { field, op: "notExists" };
|
|
252
|
+
}
|
|
253
|
+
|