@objectstack/driver-memory 0.9.2 → 1.0.1
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 +22 -0
- package/README.md +7 -221
- package/dist/memory-driver.d.ts +9 -0
- package/dist/memory-driver.d.ts.map +1 -1
- package/dist/memory-driver.js +185 -8
- package/dist/memory-driver.test.d.ts +2 -0
- package/dist/memory-driver.test.d.ts.map +1 -0
- package/dist/memory-driver.test.js +93 -0
- package/dist/memory-matcher.d.ts +19 -0
- package/dist/memory-matcher.d.ts.map +1 -0
- package/dist/memory-matcher.js +160 -0
- package/package.json +9 -5
- package/src/memory-driver.test.ts +120 -0
- package/src/memory-driver.ts +213 -9
- package/src/memory-matcher.ts +167 -0
|
@@ -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
|
+
}
|