@objectstack/driver-memory 0.9.2 → 1.0.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.
@@ -0,0 +1,167 @@
1
+
2
+ /**
3
+ * Simple In-Memory Query Matcher
4
+ *
5
+ * Implements a subset of the ObjectStack Filter Protocol (MongoDB-compatible)
6
+ * for evaluating conditions against in-memory JavaScript objects.
7
+ */
8
+
9
+ type RecordType = Record<string, any>;
10
+
11
+ /**
12
+ * matches - Check if a record matches a filter criteria
13
+ * @param record The data record to check
14
+ * @param filter The filter condition (where clause)
15
+ */
16
+ export function match(record: RecordType, filter: any): boolean {
17
+ if (!filter || Object.keys(filter).length === 0) return true;
18
+
19
+ // 1. Handle Top-Level Logical Operators ($and, $or, $not)
20
+ // These usually appear at the root or nested.
21
+
22
+ // $and: [ { ... }, { ... } ]
23
+ if (Array.isArray(filter.$and)) {
24
+ if (!filter.$and.every((f: any) => match(record, f))) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ // $or: [ { ... }, { ... } ]
30
+ if (Array.isArray(filter.$or)) {
31
+ if (!filter.$or.some((f: any) => match(record, f))) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ // $not: { ... }
37
+ if (filter.$not) {
38
+ if (match(record, filter.$not)) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ // 2. Iterate over field constraints
44
+ for (const key of Object.keys(filter)) {
45
+ // Skip logical operators we already handled (or future ones)
46
+ if (key.startsWith('$')) continue;
47
+
48
+ const condition = filter[key];
49
+ const value = getValueByPath(record, key);
50
+
51
+ if (!checkCondition(value, condition)) {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Access nested properties via dot-notation (e.g. "user.name")
61
+ */
62
+ export function getValueByPath(obj: any, path: string): any {
63
+ if (!path.includes('.')) return obj[path];
64
+ return path.split('.').reduce((o, i) => (o ? o[i] : undefined), obj);
65
+ }
66
+
67
+ /**
68
+ * Evaluate a specific condition against a value
69
+ */
70
+ function checkCondition(value: any, condition: any): boolean {
71
+ // Case A: Implicit Equality (e.g. status: 'active')
72
+ // If condition is a primitive or Date/Array (exact match), treat as equality.
73
+ if (
74
+ typeof condition !== 'object' ||
75
+ condition === null ||
76
+ condition instanceof Date ||
77
+ Array.isArray(condition)
78
+ ) {
79
+ // Loose equality to handle undefined/null mismatch or string/number coercion if desired.
80
+ // But stick to == for JS loose equality which is often convenient in weakly typed queries.
81
+ return value == condition;
82
+ }
83
+
84
+ // Case B: Operator Object (e.g. { $gt: 10, $lt: 20 })
85
+ const keys = Object.keys(condition);
86
+ const isOperatorObject = keys.some(k => k.startsWith('$'));
87
+
88
+ if (!isOperatorObject) {
89
+ // It's just a nested object comparison or implicit equality against an object
90
+ // Simplistic check:
91
+ return JSON.stringify(value) === JSON.stringify(condition);
92
+ }
93
+
94
+ // Iterate operators
95
+ for (const op of keys) {
96
+ const target = condition[op];
97
+
98
+ // Handle undefined values
99
+ if (value === undefined && op !== '$exists' && op !== '$ne') {
100
+ return false;
101
+ }
102
+
103
+ switch (op) {
104
+ case '$eq':
105
+ if (value != target) return false;
106
+ break;
107
+ case '$ne':
108
+ if (value == target) return false;
109
+ break;
110
+
111
+ // Numeric / Date
112
+ case '$gt':
113
+ if (!(value > target)) return false;
114
+ break;
115
+ case '$gte':
116
+ if (!(value >= target)) return false;
117
+ break;
118
+ case '$lt':
119
+ if (!(value < target)) return false;
120
+ break;
121
+ case '$lte':
122
+ if (!(value <= target)) return false;
123
+ break;
124
+ case '$between':
125
+ // target should be [min, max]
126
+ if (Array.isArray(target) && (value < target[0] || value > target[1])) return false;
127
+ break;
128
+
129
+ // Sets
130
+ case '$in':
131
+ if (!Array.isArray(target) || !target.includes(value)) return false;
132
+ break;
133
+ case '$nin':
134
+ if (Array.isArray(target) && target.includes(value)) return false;
135
+ break;
136
+
137
+ // Existence
138
+ case '$exists':
139
+ const exists = value !== undefined && value !== null;
140
+ if (exists !== !!target) return false;
141
+ break;
142
+
143
+ // Strings
144
+ case '$contains':
145
+ if (typeof value !== 'string' || !value.includes(target)) return false;
146
+ break;
147
+ case '$startsWith':
148
+ if (typeof value !== 'string' || !value.startsWith(target)) return false;
149
+ break;
150
+ case '$endsWith':
151
+ if (typeof value !== 'string' || !value.endsWith(target)) return false;
152
+ break;
153
+ case '$regex':
154
+ try {
155
+ const re = new RegExp(target, condition.$options || '');
156
+ if (!re.test(String(value))) return false;
157
+ } catch (e) { return false; }
158
+ break;
159
+
160
+ default:
161
+ // Unknown operator, ignore or fail. Ignoring safe for optional features.
162
+ break;
163
+ }
164
+ }
165
+
166
+ return true;
167
+ }