corebasic 1.0.206 → 1.0.208
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/libs/dip/suffix/date.js +103 -0
- package/libs/dip/suffix/index.js +249 -0
- package/libs/dip/suffix/policy.js +156 -0
- package/libs/dip/suffix/query.js +289 -0
- package/libs/dip/suffix/suffix.js +109 -0
- package/libs/dip/suffix/tests/date.test.js +416 -0
- package/libs/dip/suffix/tests/index.test.js +653 -0
- package/libs/dip/suffix/tests/policy.applySuffixPolicy.test.js +642 -0
- package/libs/dip/suffix/tests/policy.test.js +675 -0
- package/libs/dip/suffix/tests/suffix.test.js +622 -0
- package/libs/dip.js +3 -3
- package/libs/elabase.js +34 -30
- package/libs/features.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Lightweight Query Matcher (JavaScript)
|
|
2
|
+
// ========= Utility Functions =========
|
|
3
|
+
|
|
4
|
+
function toF64(value) {
|
|
5
|
+
if (value === null || value === undefined) return 0;
|
|
6
|
+
if (typeof value === 'number') return value;
|
|
7
|
+
if (typeof value === 'string') {
|
|
8
|
+
const parsed = parseFloat(value);
|
|
9
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getValueAt(obj, key) {
|
|
16
|
+
if (obj === null || obj === undefined) return null;
|
|
17
|
+
if (typeof obj !== 'object') return null;
|
|
18
|
+
return obj[key] !== undefined ? obj[key] : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ========= Comparison Primitives =========
|
|
22
|
+
|
|
23
|
+
function eqQuery(v1, v2) {
|
|
24
|
+
// Deep equality for objects/arrays, strict for primitives
|
|
25
|
+
if (v1 === v2) return true;
|
|
26
|
+
if (v1 === null || v2 === null) return v1 === v2;
|
|
27
|
+
if (typeof v1 !== typeof v2) {
|
|
28
|
+
// Try numeric comparison
|
|
29
|
+
if (typeof v1 === 'number' || typeof v2 === 'number') {
|
|
30
|
+
return toF64(v1) === toF64(v2);
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (typeof v1 === 'object') {
|
|
35
|
+
if (Array.isArray(v1) !== Array.isArray(v2)) return false;
|
|
36
|
+
if (Array.isArray(v1)) {
|
|
37
|
+
if (v1.length !== v2.length) return false;
|
|
38
|
+
for (let i = 0; i < v1.length; i++) {
|
|
39
|
+
if (!eqQuery(v1[i], v2[i])) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const keys1 = Object.keys(v1);
|
|
44
|
+
const keys2 = Object.keys(v2);
|
|
45
|
+
if (keys1.length !== keys2.length) return false;
|
|
46
|
+
for (const key of keys1) {
|
|
47
|
+
if (!keys2.includes(key)) return false;
|
|
48
|
+
if (!eqQuery(v1[key], v2[key])) return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function gtQuery(v1, v2) {
|
|
56
|
+
return toF64(v1) > toF64(v2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function gteQuery(v1, v2) {
|
|
60
|
+
return toF64(v1) >= toF64(v2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ltQuery(v1, v2) {
|
|
64
|
+
return toF64(v1) < toF64(v2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function lteQuery(v1, v2) {
|
|
68
|
+
return toF64(v1) <= toF64(v2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function existsQuery(v1, v2) {
|
|
72
|
+
const exists = v1 !== null && v1 !== undefined;
|
|
73
|
+
const condition = typeof v2 === 'boolean' ? v2 : true;
|
|
74
|
+
return condition === exists;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function beginQuery(input, arg) {
|
|
78
|
+
if (typeof input === 'string' && typeof arg === 'string') {
|
|
79
|
+
return input.startsWith(arg);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function endQuery(input, arg) {
|
|
85
|
+
if (typeof input === 'string' && typeof arg === 'string') {
|
|
86
|
+
return input.endsWith(arg);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function containsQuery(input, arg, caseSensitive = true) {
|
|
92
|
+
if (typeof input !== 'string') return false;
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(arg)) {
|
|
95
|
+
if (arg.length === 0) return false;
|
|
96
|
+
let allMatch = true;
|
|
97
|
+
for (const v of arg) {
|
|
98
|
+
let needle = typeof v === 'string' ? v : String(v);
|
|
99
|
+
let haystack = input;
|
|
100
|
+
if (!caseSensitive) {
|
|
101
|
+
needle = needle.toLowerCase();
|
|
102
|
+
haystack = haystack.toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
if (!haystack.includes(needle)) {
|
|
105
|
+
allMatch = false;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return allMatch;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof arg === 'string') {
|
|
113
|
+
let needle = arg;
|
|
114
|
+
let haystack = input;
|
|
115
|
+
if (!caseSensitive) {
|
|
116
|
+
needle = needle.toLowerCase();
|
|
117
|
+
haystack = haystack.toLowerCase();
|
|
118
|
+
}
|
|
119
|
+
return haystack.includes(needle);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ========= Query Commands =========
|
|
126
|
+
|
|
127
|
+
function inQuery(input, args) {
|
|
128
|
+
if (Array.isArray(args)) {
|
|
129
|
+
for (const item of args) {
|
|
130
|
+
if (eqQuery(input, item)) return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (args !== null && typeof args === 'object') return false;
|
|
135
|
+
return eqQuery(input, args);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function elemMatchQuery(input, query) {
|
|
139
|
+
if (!Array.isArray(input)) return false;
|
|
140
|
+
|
|
141
|
+
const term = query.$ || '$';
|
|
142
|
+
const captureLimit = query.$captureLimit || 0;
|
|
143
|
+
let countMatched = 0;
|
|
144
|
+
|
|
145
|
+
for (let index = 0; index < input.length; index++) {
|
|
146
|
+
if (andQuery(input[index], query)) {
|
|
147
|
+
if (captureLimit > 0 && countMatched >= captureLimit) break;
|
|
148
|
+
countMatched++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return countMatched > 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function lengthQuery(input, arg) {
|
|
156
|
+
let len;
|
|
157
|
+
if (Array.isArray(input)) {
|
|
158
|
+
len = input.length;
|
|
159
|
+
} else if (typeof input === 'string') {
|
|
160
|
+
len = input.length;
|
|
161
|
+
} else {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (arg !== null && typeof arg === 'object' && !Array.isArray(arg)) {
|
|
166
|
+
return andQuery(len, arg);
|
|
167
|
+
}
|
|
168
|
+
if (typeof arg === 'number') {
|
|
169
|
+
return len === arg;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function icaseQuery(input, arg) {
|
|
175
|
+
if (typeof input !== 'string') return false;
|
|
176
|
+
|
|
177
|
+
const lowerInput = input.toLowerCase();
|
|
178
|
+
|
|
179
|
+
if (arg !== null && typeof arg === 'object' && !Array.isArray(arg)) {
|
|
180
|
+
return andQuery(lowerInput, arg);
|
|
181
|
+
}
|
|
182
|
+
if (typeof arg === 'string') {
|
|
183
|
+
return eqQuery(lowerInput, arg.toLowerCase());
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function selectCommand(key, input, value) {
|
|
189
|
+
switch (key) {
|
|
190
|
+
// Comparison
|
|
191
|
+
case '$':
|
|
192
|
+
return true;
|
|
193
|
+
case '$eq':
|
|
194
|
+
return eqQuery(input, value);
|
|
195
|
+
case '$ne':
|
|
196
|
+
return !eqQuery(input, value);
|
|
197
|
+
case '$gt':
|
|
198
|
+
return gtQuery(input, value);
|
|
199
|
+
case '$gte':
|
|
200
|
+
return gteQuery(input, value);
|
|
201
|
+
case '$lt':
|
|
202
|
+
return ltQuery(input, value);
|
|
203
|
+
case '$lte':
|
|
204
|
+
return lteQuery(input, value);
|
|
205
|
+
case '$in':
|
|
206
|
+
return inQuery(input, value);
|
|
207
|
+
case '$nin':
|
|
208
|
+
return !inQuery(input, value);
|
|
209
|
+
|
|
210
|
+
// Logical
|
|
211
|
+
case '$not':
|
|
212
|
+
return !andQuery(input, value);
|
|
213
|
+
case '$exists':
|
|
214
|
+
return existsQuery(input, value);
|
|
215
|
+
case '$or':
|
|
216
|
+
return orQuery(input, value);
|
|
217
|
+
case '$nor':
|
|
218
|
+
return !orQuery(input, value);
|
|
219
|
+
case '$and':
|
|
220
|
+
if (!Array.isArray(value)) return false;
|
|
221
|
+
for (const item of value) {
|
|
222
|
+
if (!andQuery(input, item)) return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
|
|
226
|
+
// Array
|
|
227
|
+
case '$elemMatch':
|
|
228
|
+
return elemMatchQuery(input, value);
|
|
229
|
+
case '$length':
|
|
230
|
+
return lengthQuery(input, value);
|
|
231
|
+
|
|
232
|
+
// String matching
|
|
233
|
+
case '$icase':
|
|
234
|
+
return icaseQuery(input, value);
|
|
235
|
+
case '$begin':
|
|
236
|
+
return beginQuery(input, value);
|
|
237
|
+
case '$end':
|
|
238
|
+
return endQuery(input, value);
|
|
239
|
+
case '$contains':
|
|
240
|
+
return containsQuery(input, value);
|
|
241
|
+
|
|
242
|
+
default:
|
|
243
|
+
if (key.startsWith('$')) return true; // Unknown $keys pass through
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function orQuery(input, value) {
|
|
249
|
+
if (!Array.isArray(value)) return false;
|
|
250
|
+
for (const item of value) {
|
|
251
|
+
if (andQuery(input, item)) return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function andQuery(input, qvalue) {
|
|
257
|
+
if (qvalue === null || qvalue === undefined) {
|
|
258
|
+
return eqQuery(input, qvalue);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof qvalue === 'object' && !Array.isArray(qvalue)) {
|
|
262
|
+
for (const [key, value] of Object.entries(qvalue)) {
|
|
263
|
+
if (key.startsWith('$')) {
|
|
264
|
+
if (!selectCommand(key, input, value)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const v = (typeof value === 'object' && value !== null && !Array.isArray(value))
|
|
269
|
+
? andQuery(getValueAt(input, key), value)
|
|
270
|
+
: eqQuery(getValueAt(input, key), value);
|
|
271
|
+
if (!v) return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (Array.isArray(qvalue)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return eqQuery(input, qvalue);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ========= Main Entry Point =========
|
|
285
|
+
|
|
286
|
+
export function search(data, query) {
|
|
287
|
+
return andQuery(data, query);
|
|
288
|
+
}
|
|
289
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {entries, reduceRanges, extractBounds, getNormalizedBounds} from './index.js'
|
|
2
|
+
import {formatDate, fillDates} from './date.js'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const EXPLICIT_SUFFIX_POLICY_TYPES = new Set([ "string", "date", "number" ]);
|
|
7
|
+
export async function suffix(doc, policies, insertMode, arg) {
|
|
8
|
+
let suffixes = [""]
|
|
9
|
+
for (const policy of policies) {
|
|
10
|
+
|
|
11
|
+
const value = typeof policy.value === "string" ? policy.value.trim() : undefined
|
|
12
|
+
if ("value" in policy && !value)
|
|
13
|
+
throw new Error(`Error: Invalid suffix policy fixed 'value' specified for key in Dip`)
|
|
14
|
+
|
|
15
|
+
let keys = value ? [value] : entries(policy.key, doc);
|
|
16
|
+
const [policy_type, format] = (policy.type ?? "string").split(':')
|
|
17
|
+
|
|
18
|
+
if (!EXPLICIT_SUFFIX_POLICY_TYPES.has(policy_type))
|
|
19
|
+
throw new Error(`Error: Invalid suffix policy type specified for key ${policy.key} in Dip`)
|
|
20
|
+
|
|
21
|
+
if (policy_type === "string" && keys.filter(item => typeof item === "object").length)
|
|
22
|
+
throw new Error(`Error: Invalid numeric bounds on string type in suffix policy specified for key ${policy.key} in Dip`)
|
|
23
|
+
|
|
24
|
+
if (policy_type === "date") {
|
|
25
|
+
if (!format?.trim())
|
|
26
|
+
throw new Error(`Error: Invalid date format in suffix policy type specified for key ${policy.key} in Dip`)
|
|
27
|
+
if (format.includes('/'))
|
|
28
|
+
throw new Error(`Error: Invalid date format containing '/' in suffix policy type specified for key ${policy.key} in Dip`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ls_threshold = policy.ls_threshold ?? 5000
|
|
32
|
+
|
|
33
|
+
// ====== Range ======
|
|
34
|
+
let ranges = keys.filter(item => typeof item === "object")
|
|
35
|
+
keys = keys.filter(item => typeof item !== "object")
|
|
36
|
+
|
|
37
|
+
if (policy_type === "date") {
|
|
38
|
+
keys = keys.map(key => formatDate(key, format))
|
|
39
|
+
if (ranges.length) {
|
|
40
|
+
ranges = extractBounds(reduceRanges(ranges))
|
|
41
|
+
const from = ranges.from ?? policy.min // policy.min is always inclusive i.e $gte
|
|
42
|
+
const to = ranges.to ?? policy.max // policy.max is always inclusive i.e $lte
|
|
43
|
+
const fromOp = ranges.from ? ranges.fromOp : "$gte" // policy.min is $gte
|
|
44
|
+
const toOp = ranges.to ? ranges.toOp : "$lte" // policy.max is $lte
|
|
45
|
+
if (from === undefined || to === undefined) {
|
|
46
|
+
keys = [] // Fall back to Dip.operation("ls").
|
|
47
|
+
console.warn(`Warn: Missing suffix policy min/max bound for ${policy_type} range specified for key ${policy.key} in Dip`)
|
|
48
|
+
} else {
|
|
49
|
+
ranges = fillDates({from, to, fromOp, toOp}, format)
|
|
50
|
+
// console.log(ranges)
|
|
51
|
+
if (ranges.length > ls_threshold) {
|
|
52
|
+
keys = [] // Too many suffixes to enumerate. Fall back to Dip.operation("ls").
|
|
53
|
+
console.warn(`Warn: Suffix ls_threshold exceeded for key ${policy.key} in Dip`)
|
|
54
|
+
} else
|
|
55
|
+
keys.push(...ranges)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
keys = [...new Set(keys)];
|
|
59
|
+
} else if (policy_type === "number" && ranges.length) {
|
|
60
|
+
ranges = getNormalizedBounds(extractBounds(reduceRanges(ranges)), "number")
|
|
61
|
+
const from = ranges.from ?? policy.min // policy.min is always inclusive i.e $gte
|
|
62
|
+
const to = ranges.to ?? policy.max // policy.max is always inclusive i.e $lte
|
|
63
|
+
if (from === undefined || to === undefined) {
|
|
64
|
+
keys = [] // Fall back to Dip.operation("ls").
|
|
65
|
+
console.warn(`Warn: Missing suffix policy min/max bound for ${policy_type} range specified for key ${policy.key} in Dip`)
|
|
66
|
+
} else {
|
|
67
|
+
if (to - from + 1 > ls_threshold) {
|
|
68
|
+
keys = [] // Too many suffixes to enumerate. Fall back to Dip.operation("ls").
|
|
69
|
+
console.warn(`Warn: Suffix ls_threshold exceeded for key ${policy.key} in Dip`)
|
|
70
|
+
} else {
|
|
71
|
+
for (let i = from; i <= to; i++)
|
|
72
|
+
keys.push(i)
|
|
73
|
+
}
|
|
74
|
+
keys = [...new Set(keys)];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// =================
|
|
78
|
+
|
|
79
|
+
let ls = false
|
|
80
|
+
let suffixAssociatedKeys = {}
|
|
81
|
+
|
|
82
|
+
if (!keys.length && insertMode)
|
|
83
|
+
throw new Error(`Error: Missing suffix policy key ${policy.key} in Dip`)
|
|
84
|
+
if (!keys.length && !insertMode) {
|
|
85
|
+
ls = true
|
|
86
|
+
for (const suffix of suffixes) {
|
|
87
|
+
const dirs = (await Dip.operation("ls", { db: arg.db, collection: arg.collection, suffix })).filter(dir => !dir.startsWith('chunk-'))
|
|
88
|
+
suffixAssociatedKeys[suffix] = dirs
|
|
89
|
+
}
|
|
90
|
+
console.warn(`Warn: Query omitted suffix policy key ${policy.key} in Dip. Falling back to Dip.operation(ls), incurring additional performance and network round-trip overhead.`)
|
|
91
|
+
}
|
|
92
|
+
if (!ls && keys.some(key => typeof key !== "number" && typeof key !== "string")) // Dates are already string by this point
|
|
93
|
+
throw new Error(`Error: Invalid value for suffix policy key ${policy.key} in Dip`)
|
|
94
|
+
|
|
95
|
+
// =================
|
|
96
|
+
|
|
97
|
+
const next = [];
|
|
98
|
+
for (const suffix of suffixes) {
|
|
99
|
+
const currentKeys = ls ? suffixAssociatedKeys[suffix] : keys;
|
|
100
|
+
for (const key of currentKeys) {
|
|
101
|
+
next.push(`${suffix}/${key}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
suffixes = next; // The "dead branches" disappear automatically because they never contribute any children.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
return suffixes.length === 1 && suffixes[0] === "" ? [] : suffixes
|
|
109
|
+
}
|