@spotto/semantic-query 1.0.70-alpha.26 → 1.0.70-alpha.27
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/dates.d.ts +32 -32
- package/dist/dates.js +104 -104
- package/dist/evaluate.d.ts +28 -28
- package/dist/evaluate.js +545 -509
- package/dist/evaluate.js.map +1 -1
- package/dist/fixtures.d.ts +277 -248
- package/dist/fixtures.js +175 -155
- package/dist/fixtures.js.map +1 -1
- package/dist/index.d.ts +32 -32
- package/dist/index.js +48 -48
- package/dist/resolve-name.d.ts +9 -9
- package/dist/resolve-name.js +90 -90
- package/dist/suggest.d.ts +12 -12
- package/dist/suggest.js +69 -69
- package/dist/types.d.ts +111 -111
- package/dist/types.js +23 -23
- package/dist/validate.d.ts +33 -33
- package/dist/validate.js +795 -754
- package/dist/validate.js.map +1 -1
- package/package.json +3 -3
package/dist/evaluate.js
CHANGED
|
@@ -1,510 +1,546 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.evaluateSemanticQuery = void 0;
|
|
4
|
-
// Workspace Dependencies
|
|
5
|
-
const contract_1 = require("@spotto/contract");
|
|
6
|
-
// Project Dependencies
|
|
7
|
-
const dates_1 = require("./dates");
|
|
8
|
-
const resolve_name_1 = require("./resolve-name");
|
|
9
|
-
const validate_1 = require("./validate");
|
|
10
|
-
const SLOT = {
|
|
11
|
-
STRING: 'valueString',
|
|
12
|
-
INTEGER: 'valueInteger',
|
|
13
|
-
DECIMAL: 'valueDecimal',
|
|
14
|
-
BOOLEAN: 'valueBoolean',
|
|
15
|
-
DATE: 'valueDate',
|
|
16
|
-
};
|
|
17
|
-
function get(doc, path) {
|
|
18
|
-
return path.split('.').reduce((a, k) => (a == null ? undefined : a[k]), doc);
|
|
19
|
-
}
|
|
20
|
-
function cmp(actual, op, target) {
|
|
21
|
-
if (actual === undefined || actual === null)
|
|
22
|
-
return false;
|
|
23
|
-
switch (op) {
|
|
24
|
-
case 'eq':
|
|
25
|
-
return actual === target;
|
|
26
|
-
case 'ne':
|
|
27
|
-
return actual !== target;
|
|
28
|
-
case 'gt':
|
|
29
|
-
return actual > target;
|
|
30
|
-
case 'gte':
|
|
31
|
-
return actual >= target;
|
|
32
|
-
case 'lt':
|
|
33
|
-
return actual < target;
|
|
34
|
-
case 'lte':
|
|
35
|
-
return actual <= target;
|
|
36
|
-
default:
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function evaluateSemanticQuery(query, docs, schema, opts = {}) {
|
|
41
|
-
var _a, _b, _c;
|
|
42
|
-
const ctx = {
|
|
43
|
-
schema,
|
|
44
|
-
now: (_a = opts.now) !== null && _a !== void 0 ? _a : new Date(),
|
|
45
|
-
tz: (_b = opts.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
|
|
46
|
-
fieldByName: new Map(schema.fields.map((f) => [f.name.toLowerCase(), f])),
|
|
47
|
-
typeById: new Map(schema.types.map((t) => [t.id, t])),
|
|
48
|
-
entityType: (_c = opts.entityType) !== null && _c !== void 0 ? _c : 'asset',
|
|
49
|
-
};
|
|
50
|
-
return docs.filter((d) => query.conditions.every((c) => match(d, c, ctx)));
|
|
51
|
-
}
|
|
52
|
-
exports.evaluateSemanticQuery = evaluateSemanticQuery;
|
|
53
|
-
/**
|
|
54
|
-
* Grouping is shared — it is structure, not subject matter — and everything
|
|
55
|
-
* else dispatches to the entity's own matcher, mirroring the Mongo compiler so
|
|
56
|
-
* the two engines are organised the same way and can be read side by side.
|
|
57
|
-
*/
|
|
58
|
-
function match(doc, cond, ctx) {
|
|
59
|
-
if (cond.type === 'and')
|
|
60
|
-
return cond.conditions.every((c) => match(doc, c, ctx));
|
|
61
|
-
if (cond.type === 'or')
|
|
62
|
-
return cond.conditions.some((c) => match(doc, c, ctx));
|
|
63
|
-
if (ctx.entityType === 'location') {
|
|
64
|
-
return matchLocation(doc, cond, ctx);
|
|
65
|
-
}
|
|
66
|
-
if (ctx.entityType === 'event') {
|
|
67
|
-
return matchEvent(doc, cond, ctx);
|
|
68
|
-
}
|
|
69
|
-
if (ctx.entityType === 'snapshot') {
|
|
70
|
-
return matchSnapshot(doc, cond, ctx);
|
|
71
|
-
}
|
|
72
|
-
if (ctx.entityType === 'reader') {
|
|
73
|
-
return matchReader(doc, cond);
|
|
74
|
-
}
|
|
75
|
-
return matchAsset(doc, cond, ctx);
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Reads the SERVER event document. Events are never evaluated in the browser —
|
|
79
|
-
* the client caches query RESULTS, not events, so there is no corpus to run
|
|
80
|
-
* over. This matcher exists as the ORACLE: the acceptance property
|
|
81
|
-
* `evaluate(q, docs) === find(compile(q))` is what catches an encoding mistake
|
|
82
|
-
* in the Mongo compiler, and an entity with no second implementation has
|
|
83
|
-
* nothing checking it but the person who wrote it.
|
|
84
|
-
*
|
|
85
|
-
* The three name-resolved words compare against the ids VALIDATION already
|
|
86
|
-
* resolved, from the same catalogue the compiler uses — so the two engines
|
|
87
|
-
* cannot disagree about who "Sarah" is.
|
|
88
|
-
*/
|
|
89
|
-
function matchEvent(doc, cond, ctx) {
|
|
90
|
-
var _a, _b, _c, _d, _e;
|
|
91
|
-
switch (cond.type) {
|
|
92
|
-
case 'occurred':
|
|
93
|
-
return cmpTimestamp(get(doc, 'timestamp'), cond.operator, cond.value, ctx);
|
|
94
|
-
case 'eventType':
|
|
95
|
-
return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'type'));
|
|
96
|
-
case 'assetType': {
|
|
97
|
-
// BOTH halves, exactly as `action` below and exactly as the compiler:
|
|
98
|
-
// `thing` is a full asset only on the CRUD events, so the type scope is
|
|
99
|
-
// part of the condition rather than something a caller must remember.
|
|
100
|
-
// Checked FIRST — a movement event has no `thing.typePath` at all, and
|
|
101
|
-
// reading one would only ever produce a confusing miss.
|
|
102
|
-
if (!contract_1.ASSET_CRUD_EVENT_TYPES.includes(String(get(doc, 'type')))) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const actual = String((_b = get(doc, 'thing.typePath')) !== null && _b !== void 0 ? _b : '');
|
|
106
|
-
return cond.includeSubtypes !== false
|
|
107
|
-
? actual.startsWith(cond.path)
|
|
108
|
-
: actual === cond.path;
|
|
109
|
-
}
|
|
110
|
-
case 'location': {
|
|
111
|
-
// The stored pipe path, exactly as a location document keeps it — events
|
|
112
|
-
// embed it under `location.name`.
|
|
113
|
-
const actual = String((_c = get(doc, 'location.name')) !== null && _c !== void 0 ? _c : '');
|
|
114
|
-
return cond.includeSublocations !== false
|
|
115
|
-
? actual.startsWith(cond.path)
|
|
116
|
-
: actual === cond.path;
|
|
117
|
-
}
|
|
118
|
-
case 'reader':
|
|
119
|
-
return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
|
|
120
|
-
case 'user':
|
|
121
|
-
// A resolved user id inherently excludes system-generated events — Slate
|
|
122
|
-
// movements carry no user at all, and must never answer "what did X do".
|
|
123
|
-
return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
|
|
124
|
-
case 'action': {
|
|
125
|
-
// An action implies a submission, so both halves are checked — matching
|
|
126
|
-
// the compiler, which constrains the type for the same reason.
|
|
127
|
-
// Both submission types — see the compiler's note.
|
|
128
|
-
if (!SUBMISSION_EVENT_TYPES.includes(String(get(doc, 'type'))))
|
|
129
|
-
return false;
|
|
130
|
-
// Resolve ONCE — resolving inside the predicate re-ran it per action.
|
|
131
|
-
const ids = resolvedIds(ctx.schema.actions, cond.name);
|
|
132
|
-
const names = ((_d = ctx.schema.actions) !== null && _d !== void 0 ? _d : [])
|
|
133
|
-
.filter((a) => ids.includes(a.id))
|
|
134
|
-
.map((a) => a.name);
|
|
135
|
-
return names.includes(String((_e = get(doc, 'submission.actionName')) !== null && _e !== void 0 ? _e : ''));
|
|
136
|
-
}
|
|
137
|
-
default:
|
|
138
|
-
// LOUD, not false. This function is the ORACLE the Mongo compiler is
|
|
139
|
-
// checked against, and an oracle that silently matches nothing AGREES
|
|
140
|
-
// with a compiler that returns nothing — so a word added to the
|
|
141
|
-
// vocabulary and the compiler but forgotten here would let the
|
|
142
|
-
// differential check pass by both engines being wrong together.
|
|
143
|
-
throw new Error(`semantic-query evaluator has no arm for event condition ` +
|
|
144
|
-
`'${cond.type}'`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Reads the SERVER snapshot document.
|
|
149
|
-
*
|
|
150
|
-
* Unlike the event matcher, this one is NOT only an oracle — it is the
|
|
151
|
-
* production path. Snapshots are cached whole and the list filters them in the
|
|
152
|
-
* browser (`useSnapshotList` → `db.snapshots.toArray()`), so a mistake here is
|
|
153
|
-
* a wrong answer a user sees, not a test that fails. The Mongo compiler still
|
|
154
|
-
* has to agree with it for saved queries and widgets.
|
|
155
|
-
*
|
|
156
|
-
* Two arms below are not the obvious reading of the field, and both are
|
|
157
|
-
* copied from the flat filter (`snapshots/filters.ts`) rather than invented,
|
|
158
|
-
* because chip and query must return the same rows.
|
|
159
|
-
*/
|
|
160
|
-
function matchSnapshot(doc, cond, ctx) {
|
|
161
|
-
var _a, _b, _c;
|
|
162
|
-
switch (cond.type) {
|
|
163
|
-
case 'occurred':
|
|
164
|
-
// `submitted`, not `created` — the flat time filter's field. It is
|
|
165
|
-
// optional, so an unsubmitted snapshot matches no bound; `cmpTimestamp`
|
|
166
|
-
// returns false for an absent value, which is the same answer the flat
|
|
167
|
-
// filter's range gives.
|
|
168
|
-
return cmpTimestamp(get(doc, 'submitted'), cond.operator, cond.value, ctx);
|
|
169
|
-
case 'snapshotType': {
|
|
170
|
-
const type = get(doc, 'type');
|
|
171
|
-
const hasManifest = get(doc, 'manifest') != null;
|
|
172
|
-
return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).some((wanted) => {
|
|
173
|
-
const rule = contract_1.SNAPSHOT_TYPE_RULES[wanted];
|
|
174
|
-
if (!rule)
|
|
175
|
-
return false;
|
|
176
|
-
if (type === rule.explicit)
|
|
177
|
-
return true;
|
|
178
|
-
// Legacy: no `type` at all, classified by the manifest's presence.
|
|
179
|
-
// Without this arm every pre-`type` snapshot silently disappears from
|
|
180
|
-
// a kind filter that the chip beside it still returns.
|
|
181
|
-
return (type == null &&
|
|
182
|
-
rule.legacyHasManifest !== undefined &&
|
|
183
|
-
hasManifest === rule.legacyHasManifest);
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
case 'asset':
|
|
187
|
-
// The snapshot's SUBJECT asset, not its contents — see the contract.
|
|
188
|
-
return idIn(get(doc, 'asset._id'), [cond.assetId]);
|
|
189
|
-
case 'assetType': {
|
|
190
|
-
const actual = String((_b = get(doc, 'asset.typePath')) !== null && _b !== void 0 ? _b : '');
|
|
191
|
-
return cond.includeSubtypes !== false
|
|
192
|
-
? actual.startsWith(cond.path)
|
|
193
|
-
: actual === cond.path;
|
|
194
|
-
}
|
|
195
|
-
case 'location': {
|
|
196
|
-
// `location.path` here, NOT `location.name` as events use. Absent on
|
|
197
|
-
// legacy snapshots, which therefore match no path condition — the same
|
|
198
|
-
// acceptance record scope already makes (usergroups S7).
|
|
199
|
-
const actual = String((_c = get(doc, 'location.path')) !== null && _c !== void 0 ? _c : '');
|
|
200
|
-
if (!actual)
|
|
201
|
-
return false;
|
|
202
|
-
return cond.includeSublocations !== false
|
|
203
|
-
? actual.startsWith(cond.path)
|
|
204
|
-
: actual === cond.path;
|
|
205
|
-
}
|
|
206
|
-
case 'manifest':
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
// `
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
case '
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
case '
|
|
370
|
-
return
|
|
371
|
-
case '
|
|
372
|
-
return
|
|
373
|
-
case '
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateSemanticQuery = void 0;
|
|
4
|
+
// Workspace Dependencies
|
|
5
|
+
const contract_1 = require("@spotto/contract");
|
|
6
|
+
// Project Dependencies
|
|
7
|
+
const dates_1 = require("./dates");
|
|
8
|
+
const resolve_name_1 = require("./resolve-name");
|
|
9
|
+
const validate_1 = require("./validate");
|
|
10
|
+
const SLOT = {
|
|
11
|
+
STRING: 'valueString',
|
|
12
|
+
INTEGER: 'valueInteger',
|
|
13
|
+
DECIMAL: 'valueDecimal',
|
|
14
|
+
BOOLEAN: 'valueBoolean',
|
|
15
|
+
DATE: 'valueDate',
|
|
16
|
+
};
|
|
17
|
+
function get(doc, path) {
|
|
18
|
+
return path.split('.').reduce((a, k) => (a == null ? undefined : a[k]), doc);
|
|
19
|
+
}
|
|
20
|
+
function cmp(actual, op, target) {
|
|
21
|
+
if (actual === undefined || actual === null)
|
|
22
|
+
return false;
|
|
23
|
+
switch (op) {
|
|
24
|
+
case 'eq':
|
|
25
|
+
return actual === target;
|
|
26
|
+
case 'ne':
|
|
27
|
+
return actual !== target;
|
|
28
|
+
case 'gt':
|
|
29
|
+
return actual > target;
|
|
30
|
+
case 'gte':
|
|
31
|
+
return actual >= target;
|
|
32
|
+
case 'lt':
|
|
33
|
+
return actual < target;
|
|
34
|
+
case 'lte':
|
|
35
|
+
return actual <= target;
|
|
36
|
+
default:
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function evaluateSemanticQuery(query, docs, schema, opts = {}) {
|
|
41
|
+
var _a, _b, _c;
|
|
42
|
+
const ctx = {
|
|
43
|
+
schema,
|
|
44
|
+
now: (_a = opts.now) !== null && _a !== void 0 ? _a : new Date(),
|
|
45
|
+
tz: (_b = opts.timeZone) !== null && _b !== void 0 ? _b : 'UTC',
|
|
46
|
+
fieldByName: new Map(schema.fields.map((f) => [f.name.toLowerCase(), f])),
|
|
47
|
+
typeById: new Map(schema.types.map((t) => [t.id, t])),
|
|
48
|
+
entityType: (_c = opts.entityType) !== null && _c !== void 0 ? _c : 'asset',
|
|
49
|
+
};
|
|
50
|
+
return docs.filter((d) => query.conditions.every((c) => match(d, c, ctx)));
|
|
51
|
+
}
|
|
52
|
+
exports.evaluateSemanticQuery = evaluateSemanticQuery;
|
|
53
|
+
/**
|
|
54
|
+
* Grouping is shared — it is structure, not subject matter — and everything
|
|
55
|
+
* else dispatches to the entity's own matcher, mirroring the Mongo compiler so
|
|
56
|
+
* the two engines are organised the same way and can be read side by side.
|
|
57
|
+
*/
|
|
58
|
+
function match(doc, cond, ctx) {
|
|
59
|
+
if (cond.type === 'and')
|
|
60
|
+
return cond.conditions.every((c) => match(doc, c, ctx));
|
|
61
|
+
if (cond.type === 'or')
|
|
62
|
+
return cond.conditions.some((c) => match(doc, c, ctx));
|
|
63
|
+
if (ctx.entityType === 'location') {
|
|
64
|
+
return matchLocation(doc, cond, ctx);
|
|
65
|
+
}
|
|
66
|
+
if (ctx.entityType === 'event') {
|
|
67
|
+
return matchEvent(doc, cond, ctx);
|
|
68
|
+
}
|
|
69
|
+
if (ctx.entityType === 'snapshot') {
|
|
70
|
+
return matchSnapshot(doc, cond, ctx);
|
|
71
|
+
}
|
|
72
|
+
if (ctx.entityType === 'reader') {
|
|
73
|
+
return matchReader(doc, cond);
|
|
74
|
+
}
|
|
75
|
+
return matchAsset(doc, cond, ctx);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Reads the SERVER event document. Events are never evaluated in the browser —
|
|
79
|
+
* the client caches query RESULTS, not events, so there is no corpus to run
|
|
80
|
+
* over. This matcher exists as the ORACLE: the acceptance property
|
|
81
|
+
* `evaluate(q, docs) === find(compile(q))` is what catches an encoding mistake
|
|
82
|
+
* in the Mongo compiler, and an entity with no second implementation has
|
|
83
|
+
* nothing checking it but the person who wrote it.
|
|
84
|
+
*
|
|
85
|
+
* The three name-resolved words compare against the ids VALIDATION already
|
|
86
|
+
* resolved, from the same catalogue the compiler uses — so the two engines
|
|
87
|
+
* cannot disagree about who "Sarah" is.
|
|
88
|
+
*/
|
|
89
|
+
function matchEvent(doc, cond, ctx) {
|
|
90
|
+
var _a, _b, _c, _d, _e;
|
|
91
|
+
switch (cond.type) {
|
|
92
|
+
case 'occurred':
|
|
93
|
+
return cmpTimestamp(get(doc, 'timestamp'), cond.operator, cond.value, ctx);
|
|
94
|
+
case 'eventType':
|
|
95
|
+
return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'type'));
|
|
96
|
+
case 'assetType': {
|
|
97
|
+
// BOTH halves, exactly as `action` below and exactly as the compiler:
|
|
98
|
+
// `thing` is a full asset only on the CRUD events, so the type scope is
|
|
99
|
+
// part of the condition rather than something a caller must remember.
|
|
100
|
+
// Checked FIRST — a movement event has no `thing.typePath` at all, and
|
|
101
|
+
// reading one would only ever produce a confusing miss.
|
|
102
|
+
if (!contract_1.ASSET_CRUD_EVENT_TYPES.includes(String(get(doc, 'type')))) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const actual = String((_b = get(doc, 'thing.typePath')) !== null && _b !== void 0 ? _b : '');
|
|
106
|
+
return cond.includeSubtypes !== false
|
|
107
|
+
? actual.startsWith(cond.path)
|
|
108
|
+
: actual === cond.path;
|
|
109
|
+
}
|
|
110
|
+
case 'location': {
|
|
111
|
+
// The stored pipe path, exactly as a location document keeps it — events
|
|
112
|
+
// embed it under `location.name`.
|
|
113
|
+
const actual = String((_c = get(doc, 'location.name')) !== null && _c !== void 0 ? _c : '');
|
|
114
|
+
return cond.includeSublocations !== false
|
|
115
|
+
? actual.startsWith(cond.path)
|
|
116
|
+
: actual === cond.path;
|
|
117
|
+
}
|
|
118
|
+
case 'reader':
|
|
119
|
+
return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
|
|
120
|
+
case 'user':
|
|
121
|
+
// A resolved user id inherently excludes system-generated events — Slate
|
|
122
|
+
// movements carry no user at all, and must never answer "what did X do".
|
|
123
|
+
return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
|
|
124
|
+
case 'action': {
|
|
125
|
+
// An action implies a submission, so both halves are checked — matching
|
|
126
|
+
// the compiler, which constrains the type for the same reason.
|
|
127
|
+
// Both submission types — see the compiler's note.
|
|
128
|
+
if (!SUBMISSION_EVENT_TYPES.includes(String(get(doc, 'type'))))
|
|
129
|
+
return false;
|
|
130
|
+
// Resolve ONCE — resolving inside the predicate re-ran it per action.
|
|
131
|
+
const ids = resolvedIds(ctx.schema.actions, cond.name);
|
|
132
|
+
const names = ((_d = ctx.schema.actions) !== null && _d !== void 0 ? _d : [])
|
|
133
|
+
.filter((a) => ids.includes(a.id))
|
|
134
|
+
.map((a) => a.name);
|
|
135
|
+
return names.includes(String((_e = get(doc, 'submission.actionName')) !== null && _e !== void 0 ? _e : ''));
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
// LOUD, not false. This function is the ORACLE the Mongo compiler is
|
|
139
|
+
// checked against, and an oracle that silently matches nothing AGREES
|
|
140
|
+
// with a compiler that returns nothing — so a word added to the
|
|
141
|
+
// vocabulary and the compiler but forgotten here would let the
|
|
142
|
+
// differential check pass by both engines being wrong together.
|
|
143
|
+
throw new Error(`semantic-query evaluator has no arm for event condition ` +
|
|
144
|
+
`'${cond.type}'`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Reads the SERVER snapshot document.
|
|
149
|
+
*
|
|
150
|
+
* Unlike the event matcher, this one is NOT only an oracle — it is the
|
|
151
|
+
* production path. Snapshots are cached whole and the list filters them in the
|
|
152
|
+
* browser (`useSnapshotList` → `db.snapshots.toArray()`), so a mistake here is
|
|
153
|
+
* a wrong answer a user sees, not a test that fails. The Mongo compiler still
|
|
154
|
+
* has to agree with it for saved queries and widgets.
|
|
155
|
+
*
|
|
156
|
+
* Two arms below are not the obvious reading of the field, and both are
|
|
157
|
+
* copied from the flat filter (`snapshots/filters.ts`) rather than invented,
|
|
158
|
+
* because chip and query must return the same rows.
|
|
159
|
+
*/
|
|
160
|
+
function matchSnapshot(doc, cond, ctx) {
|
|
161
|
+
var _a, _b, _c;
|
|
162
|
+
switch (cond.type) {
|
|
163
|
+
case 'occurred':
|
|
164
|
+
// `submitted`, not `created` — the flat time filter's field. It is
|
|
165
|
+
// optional, so an unsubmitted snapshot matches no bound; `cmpTimestamp`
|
|
166
|
+
// returns false for an absent value, which is the same answer the flat
|
|
167
|
+
// filter's range gives.
|
|
168
|
+
return cmpTimestamp(get(doc, 'submitted'), cond.operator, cond.value, ctx);
|
|
169
|
+
case 'snapshotType': {
|
|
170
|
+
const type = get(doc, 'type');
|
|
171
|
+
const hasManifest = get(doc, 'manifest') != null;
|
|
172
|
+
return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).some((wanted) => {
|
|
173
|
+
const rule = contract_1.SNAPSHOT_TYPE_RULES[wanted];
|
|
174
|
+
if (!rule)
|
|
175
|
+
return false;
|
|
176
|
+
if (type === rule.explicit)
|
|
177
|
+
return true;
|
|
178
|
+
// Legacy: no `type` at all, classified by the manifest's presence.
|
|
179
|
+
// Without this arm every pre-`type` snapshot silently disappears from
|
|
180
|
+
// a kind filter that the chip beside it still returns.
|
|
181
|
+
return (type == null &&
|
|
182
|
+
rule.legacyHasManifest !== undefined &&
|
|
183
|
+
hasManifest === rule.legacyHasManifest);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
case 'asset':
|
|
187
|
+
// The snapshot's SUBJECT asset, not its contents — see the contract.
|
|
188
|
+
return idIn(get(doc, 'asset._id'), [cond.assetId]);
|
|
189
|
+
case 'assetType': {
|
|
190
|
+
const actual = String((_b = get(doc, 'asset.typePath')) !== null && _b !== void 0 ? _b : '');
|
|
191
|
+
return cond.includeSubtypes !== false
|
|
192
|
+
? actual.startsWith(cond.path)
|
|
193
|
+
: actual === cond.path;
|
|
194
|
+
}
|
|
195
|
+
case 'location': {
|
|
196
|
+
// `location.path` here, NOT `location.name` as events use. Absent on
|
|
197
|
+
// legacy snapshots, which therefore match no path condition — the same
|
|
198
|
+
// acceptance record scope already makes (usergroups S7).
|
|
199
|
+
const actual = String((_c = get(doc, 'location.path')) !== null && _c !== void 0 ? _c : '');
|
|
200
|
+
if (!actual)
|
|
201
|
+
return false;
|
|
202
|
+
return cond.includeSublocations !== false
|
|
203
|
+
? actual.startsWith(cond.path)
|
|
204
|
+
: actual === cond.path;
|
|
205
|
+
}
|
|
206
|
+
case 'manifest': {
|
|
207
|
+
// Mirrors the compiler byte-for-byte: `satisfied` is an exact match on
|
|
208
|
+
// the frozen `c_satisfiesManifest` annotation — a snapshot that never
|
|
209
|
+
// stamped a verdict carries no flag and matches NEITHER polarity. Both
|
|
210
|
+
// keys AND together when present; the validator guarantees at least
|
|
211
|
+
// one, so reaching the end means every present key matched.
|
|
212
|
+
if (cond.manifestId !== undefined &&
|
|
213
|
+
!idIn(get(doc, 'manifest._id'), [cond.manifestId])) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (cond.satisfied !== undefined &&
|
|
217
|
+
get(doc, 'c_satisfiesManifest') !== cond.satisfied) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
case 'user':
|
|
223
|
+
return idIn(get(doc, 'user._id'), resolvedIds(ctx.schema.users, cond.name));
|
|
224
|
+
case 'reader':
|
|
225
|
+
return idIn(get(doc, 'reader._id'), resolvedIds(ctx.schema.readers, cond.name));
|
|
226
|
+
default:
|
|
227
|
+
// LOUD, for the reason the event matcher gives — and more so here, since
|
|
228
|
+
// this arm running in the browser is what the user actually sees.
|
|
229
|
+
throw new Error(`semantic-query evaluator has no arm for snapshot condition ` +
|
|
230
|
+
`'${cond.type}'`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Reads the SERVER reader document — and, like the snapshot matcher, this is
|
|
235
|
+
* PRODUCTION code: `useReaderList` holds the whole set and the list filters it
|
|
236
|
+
* in memory.
|
|
237
|
+
*
|
|
238
|
+
* No `ctx`: every reader condition is a plain field comparison. Nothing here
|
|
239
|
+
* resolves a name or a date, which is the whole reason this vocabulary is the
|
|
240
|
+
* least treacherous of the four.
|
|
241
|
+
*/
|
|
242
|
+
function matchReader(doc, cond) {
|
|
243
|
+
var _a, _b, _c;
|
|
244
|
+
switch (cond.type) {
|
|
245
|
+
case 'deviceType':
|
|
246
|
+
return ((_a = cond.values) !== null && _a !== void 0 ? _a : []).includes(get(doc, 'deviceType'));
|
|
247
|
+
case 'readerType':
|
|
248
|
+
return ((_b = cond.values) !== null && _b !== void 0 ? _b : []).includes(get(doc, 'type'));
|
|
249
|
+
case 'location': {
|
|
250
|
+
// `locationName` holds the FULL pipe path (`readers/helpers.ts` copies
|
|
251
|
+
// `location.name`, which is a location document's path). A reader with
|
|
252
|
+
// no location matches nothing rather than everything.
|
|
253
|
+
const actual = String((_c = get(doc, 'locationName')) !== null && _c !== void 0 ? _c : '');
|
|
254
|
+
if (!actual)
|
|
255
|
+
return false;
|
|
256
|
+
return cond.includeSublocations !== false
|
|
257
|
+
? actual.startsWith(cond.path)
|
|
258
|
+
: actual === cond.path;
|
|
259
|
+
}
|
|
260
|
+
case 'attachedAsset':
|
|
261
|
+
return idIn(get(doc, 'asset'), [cond.assetId]);
|
|
262
|
+
case 'online':
|
|
263
|
+
// Strict equality against the stored boolean. A reader that has never
|
|
264
|
+
// reported has no `state` at all, so it is neither online nor offline —
|
|
265
|
+
// `hasReported` is the word for that, and conflating the two is exactly
|
|
266
|
+
// what the prototype did.
|
|
267
|
+
return get(doc, 'state.online') === cond.value;
|
|
268
|
+
case 'hasReported':
|
|
269
|
+
// `state.onlineTransTime` is the marker the flat filter uses, not
|
|
270
|
+
// `state` itself.
|
|
271
|
+
return (get(doc, 'state.onlineTransTime') !== undefined) === cond.value;
|
|
272
|
+
case 'hasGeolocation':
|
|
273
|
+
return (get(doc, 'state.geolocation') != null) === cond.value;
|
|
274
|
+
default:
|
|
275
|
+
throw new Error(`semantic-query evaluator has no arm for reader condition ` +
|
|
276
|
+
`'${cond.type}'`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* An event's ONE timestamp against a boundary. No `includeNeverSet` branch:
|
|
281
|
+
* every event has a timestamp by construction, so there is no absent case to
|
|
282
|
+
* decide a polarity for.
|
|
283
|
+
*/
|
|
284
|
+
function cmpTimestamp(actual, operator, value, ctx) {
|
|
285
|
+
if (actual === undefined || actual === null)
|
|
286
|
+
return false;
|
|
287
|
+
const ms = (0, dates_1.semanticDateBoundaryMs)(value, operator, ctx.now, ctx.tz);
|
|
288
|
+
return cmp(actual, operator, ms);
|
|
289
|
+
}
|
|
290
|
+
/** The event types that carry `submission.actionName`. */
|
|
291
|
+
const SUBMISSION_EVENT_TYPES = ['FormSubmitted', 'FormSubmissionVoided'];
|
|
292
|
+
/** Ids a name resolves to; empty when it does not resolve (validation refused it first). */
|
|
293
|
+
function resolvedIds(catalogue, name) {
|
|
294
|
+
return (0, resolve_name_1.resolveName)(catalogue, name, { path: '', noun: '' }).ids;
|
|
295
|
+
}
|
|
296
|
+
function idIn(actual, ids) {
|
|
297
|
+
// Case-insensitive: `String(ObjectId)` is lowercase hex, but a caller-built
|
|
298
|
+
// catalogue may carry uppercase (the wire's own id check accepts either).
|
|
299
|
+
// Comparing raw would make the ORACLE miss rows the compiler matches — and
|
|
300
|
+
// an oracle that under-matches agrees with a broken compiler.
|
|
301
|
+
if (actual == null)
|
|
302
|
+
return false;
|
|
303
|
+
const a = String(actual).toLowerCase();
|
|
304
|
+
return ids.some((id) => id.toLowerCase() === a);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Reads the SERVER document shape, as the asset matcher does — a client
|
|
308
|
+
* caching parsed responses maps into it first (`evaluable-*`).
|
|
309
|
+
*
|
|
310
|
+
* Two words differ from their asset namesakes and both would fail silently:
|
|
311
|
+
* the pipe path lives in `name` (a location document has no `path` field), and
|
|
312
|
+
* `geolocation` is the CONFIGURED position at the top level, not the observed
|
|
313
|
+
* `state.geolocation` an asset's condition reads.
|
|
314
|
+
*/
|
|
315
|
+
function matchLocation(doc, cond, ctx) {
|
|
316
|
+
var _a, _b;
|
|
317
|
+
switch (cond.type) {
|
|
318
|
+
case 'path':
|
|
319
|
+
return prefixMatch(String((_a = get(doc, 'name')) !== null && _a !== void 0 ? _a : ''), cond.path, cond.includeSublocations !== false);
|
|
320
|
+
case 'locationType':
|
|
321
|
+
return ((_b = cond.values) !== null && _b !== void 0 ? _b : []).includes(get(doc, 'type'));
|
|
322
|
+
case 'hasReaders': {
|
|
323
|
+
const readers = get(doc, 'readers');
|
|
324
|
+
return (Array.isArray(readers) && readers.length > 0) === cond.value;
|
|
325
|
+
}
|
|
326
|
+
case 'geofence': {
|
|
327
|
+
// `!= null` is deliberate (missing OR explicit null), matching the
|
|
328
|
+
// compiler's `{geofence: null}` partition exactly. `!== undefined` alone
|
|
329
|
+
// would put a null geofence on the opposite side from Mongo.
|
|
330
|
+
const v = get(doc, 'geofence');
|
|
331
|
+
return cond.exists ? v != null : v == null;
|
|
332
|
+
}
|
|
333
|
+
case 'manifest': {
|
|
334
|
+
const ids = get(doc, 'manifestIds');
|
|
335
|
+
return Array.isArray(ids) && ids.some((id) => String(id) === cond.manifestId);
|
|
336
|
+
}
|
|
337
|
+
case 'geolocation': {
|
|
338
|
+
const v = get(doc, 'geolocation');
|
|
339
|
+
return cond.exists !== false ? v !== undefined : v === undefined;
|
|
340
|
+
}
|
|
341
|
+
case 'tags':
|
|
342
|
+
return matchTags(doc, cond);
|
|
343
|
+
// No lastSeen / firstSeen / telemetry — see the vocabulary in the contract.
|
|
344
|
+
case 'createdDate':
|
|
345
|
+
case 'lastUpdated':
|
|
346
|
+
case 'lastChanged':
|
|
347
|
+
return matchTimestamp(doc, cond, ctx);
|
|
348
|
+
default:
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function matchTags(doc, cond) {
|
|
353
|
+
var _a, _b;
|
|
354
|
+
const tags = ((_a = doc.tagIds) !== null && _a !== void 0 ? _a : []).map(String);
|
|
355
|
+
const want = ((_b = cond.values) !== null && _b !== void 0 ? _b : []).map(String);
|
|
356
|
+
return cond.operator === 'all'
|
|
357
|
+
? want.every((t) => tags.includes(t))
|
|
358
|
+
: want.some((t) => tags.includes(t));
|
|
359
|
+
}
|
|
360
|
+
function matchAsset(doc, cond, ctx) {
|
|
361
|
+
var _a, _b, _c, _d;
|
|
362
|
+
switch (cond.type) {
|
|
363
|
+
case 'and':
|
|
364
|
+
return cond.conditions.every((c) => match(doc, c, ctx));
|
|
365
|
+
case 'or':
|
|
366
|
+
return cond.conditions.some((c) => match(doc, c, ctx));
|
|
367
|
+
case 'assetType':
|
|
368
|
+
return prefixMatch(String((_a = get(doc, 'typePath')) !== null && _a !== void 0 ? _a : ''), cond.path, cond.includeSubtypes !== false);
|
|
369
|
+
case 'location':
|
|
370
|
+
return prefixMatch(String((_b = get(doc, 'state.locationName')) !== null && _b !== void 0 ? _b : ''), cond.path, cond.includeSublocations !== false);
|
|
371
|
+
case 'homeLocation':
|
|
372
|
+
return prefixMatch(String((_c = get(doc, 'homeLocationPath')) !== null && _c !== void 0 ? _c : ''), cond.path, cond.includeSublocations !== false);
|
|
373
|
+
case 'withAsset': {
|
|
374
|
+
const v = get(doc, 'state.locationWithId');
|
|
375
|
+
if (cond.hasAny !== undefined)
|
|
376
|
+
return (v !== undefined && v !== null) === cond.hasAny;
|
|
377
|
+
return v != null && String(v) === cond.assetId;
|
|
378
|
+
}
|
|
379
|
+
case 'geolocation': {
|
|
380
|
+
const v = get(doc, 'state.geolocation');
|
|
381
|
+
return cond.exists !== false ? v !== undefined : v === undefined;
|
|
382
|
+
}
|
|
383
|
+
case 'tags':
|
|
384
|
+
return matchTags(doc, cond);
|
|
385
|
+
case 'telemetry':
|
|
386
|
+
return cmp(get(doc, `telemetry.${cond.field}`), cond.operator, cond.value);
|
|
387
|
+
case 'readiness': {
|
|
388
|
+
const levels = (_d = cond.levels) !== null && _d !== void 0 ? _d : (cond.level === undefined ? [] : [cond.level]);
|
|
389
|
+
// Missing readiness ≡ green (0).
|
|
390
|
+
const actual = get(doc, 'readiness.ready');
|
|
391
|
+
const effective = typeof actual === 'number' ? actual : 0;
|
|
392
|
+
return levels.includes(effective);
|
|
393
|
+
}
|
|
394
|
+
case 'kit': {
|
|
395
|
+
if (cond.parentAssetId !== undefined) {
|
|
396
|
+
const p = get(doc, 'groupParentId');
|
|
397
|
+
return p != null && String(p) === cond.parentAssetId;
|
|
398
|
+
}
|
|
399
|
+
if (cond.isKit !== undefined)
|
|
400
|
+
return (doc.typeKit === true) === cond.isKit;
|
|
401
|
+
if (cond.isMember !== undefined)
|
|
402
|
+
return (doc.groupMember === true) === cond.isMember;
|
|
403
|
+
// The 4-state manifest-check status, mirroring the Mongo compile
|
|
404
|
+
// exactly — `neverChecked` carries the same typeKit self-scope, and
|
|
405
|
+
// every state requires `hasManifest` (a stored status outlives a
|
|
406
|
+
// manifest detach as inert history; the gate keeps the filter agreeing
|
|
407
|
+
// with the display).
|
|
408
|
+
if (cond.manifest !== undefined) {
|
|
409
|
+
if (doc.hasManifest !== true)
|
|
410
|
+
return false;
|
|
411
|
+
const status = doc.groupSatisfiesManifest;
|
|
412
|
+
switch (cond.manifest) {
|
|
413
|
+
case 'satisfied':
|
|
414
|
+
return status === 0;
|
|
415
|
+
case 'needsRecheck':
|
|
416
|
+
return status === 1;
|
|
417
|
+
case 'notSatisfied':
|
|
418
|
+
return status === 2;
|
|
419
|
+
case 'neverChecked':
|
|
420
|
+
return doc.typeKit === true && status === undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Legacy `satisfied` boolean over the 4-state `groupSatisfiesManifest`
|
|
424
|
+
// (0 satisfied · 1 needs-recheck · 2 not satisfied · absent never
|
|
425
|
+
// checked): true → 0, false → 2, exactly mirroring the Mongo compile —
|
|
426
|
+
// including its `hasManifest` gate. Exact value matches — absent means
|
|
427
|
+
// no manifest check has ever stamped the field, so it is neither
|
|
428
|
+
// satisfied NOR unsatisfied.
|
|
429
|
+
return (doc.hasManifest === true &&
|
|
430
|
+
doc.groupSatisfiesManifest === (cond.satisfied ? 0 : 2));
|
|
431
|
+
}
|
|
432
|
+
case 'locationStatus': {
|
|
433
|
+
const status = get(doc, 'state.locationStatus');
|
|
434
|
+
if (typeof status === 'string' && cond.values.includes(status))
|
|
435
|
+
return true;
|
|
436
|
+
// NEVERSEEN also means "no state at all".
|
|
437
|
+
return cond.values.includes('NEVERSEEN') && get(doc, 'state') === undefined;
|
|
438
|
+
}
|
|
439
|
+
case 'createdDate':
|
|
440
|
+
case 'lastUpdated':
|
|
441
|
+
case 'lastChanged':
|
|
442
|
+
case 'lastSeen':
|
|
443
|
+
case 'firstSeen':
|
|
444
|
+
return matchTimestamp(doc, cond, ctx);
|
|
445
|
+
case 'customField':
|
|
446
|
+
return matchCustomField(doc, cond, ctx);
|
|
447
|
+
default:
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function prefixMatch(actual, path, includeDescendants) {
|
|
452
|
+
return includeDescendants ? actual.startsWith(path) : actual === path;
|
|
453
|
+
}
|
|
454
|
+
const TIMESTAMP_FIELD = {
|
|
455
|
+
lastUpdated: 'lastUpdated',
|
|
456
|
+
lastChanged: 'lastChanged',
|
|
457
|
+
lastSeen: 'state.lastSeen',
|
|
458
|
+
firstSeen: 'state.firstSeen',
|
|
459
|
+
};
|
|
460
|
+
function matchTimestamp(doc, cond, ctx) {
|
|
461
|
+
const ms = (0, dates_1.semanticDateBoundaryMs)(cond.value, cond.operator, ctx.now, ctx.tz);
|
|
462
|
+
if (cond.type === 'createdDate') {
|
|
463
|
+
// The ObjectId's first 4 bytes are the creation time in SECONDS. The
|
|
464
|
+
// compiler steps `lte`/`gt` to the next second (a zero-tail boundary id
|
|
465
|
+
// sorts before every real id in the same second); mirror that exactly,
|
|
466
|
+
// or documents created within the boundary second classify differently.
|
|
467
|
+
const created = parseInt(String(doc._id).slice(0, 8), 16);
|
|
468
|
+
const second = Math.floor(ms / 1000);
|
|
469
|
+
switch (cond.operator) {
|
|
470
|
+
case 'gte':
|
|
471
|
+
return created >= second;
|
|
472
|
+
case 'lt':
|
|
473
|
+
return created < second;
|
|
474
|
+
case 'lte':
|
|
475
|
+
return created < second + 1;
|
|
476
|
+
default: // gt
|
|
477
|
+
return created >= second + 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const actual = get(doc, TIMESTAMP_FIELD[cond.type]);
|
|
481
|
+
const neverSet = actual === undefined || actual === null;
|
|
482
|
+
if (neverSet) {
|
|
483
|
+
return ((cond.operator === 'lt' || cond.operator === 'lte') &&
|
|
484
|
+
cond.includeNeverSet !== false);
|
|
485
|
+
}
|
|
486
|
+
return cmp(actual, cond.operator, ms);
|
|
487
|
+
}
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
// Custom fields — mirror of the compiler's two-container conjunct
|
|
490
|
+
// ---------------------------------------------------------------------------
|
|
491
|
+
/**
|
|
492
|
+
* Every value the field holds in this container.
|
|
493
|
+
*
|
|
494
|
+
* Matched by field `_id` ONLY — the compiler's `$elemMatch: {_id: …}` cannot
|
|
495
|
+
* match a name — and ALL matching entries are returned, because `$elemMatch`
|
|
496
|
+
* is existential: a duplicated field where one entry is null and another
|
|
497
|
+
* holds the value must match, so inspecting only the first entry would
|
|
498
|
+
* disagree with Mongo.
|
|
499
|
+
*/
|
|
500
|
+
function valuesFor(entries, fdef, slot) {
|
|
501
|
+
return (entries !== null && entries !== void 0 ? entries : [])
|
|
502
|
+
.filter((e) => (e === null || e === void 0 ? void 0 : e._id) !== undefined && String(e._id) === fdef.id)
|
|
503
|
+
.map((e) => e[slot]);
|
|
504
|
+
}
|
|
505
|
+
function isPresent(v) {
|
|
506
|
+
return v !== undefined && v !== null && v !== '';
|
|
507
|
+
}
|
|
508
|
+
function matchCustomField(doc, cond, ctx) {
|
|
509
|
+
var _a;
|
|
510
|
+
const fdef = ctx.fieldByName.get(String(cond.fieldName).toLowerCase());
|
|
511
|
+
if (!fdef)
|
|
512
|
+
return false;
|
|
513
|
+
const slot = SLOT[fdef.dataType];
|
|
514
|
+
// The in-memory equivalent of the joinedType $lookup: the asset's own
|
|
515
|
+
// fieldValues entries, and its type's inheritance-resolved entries.
|
|
516
|
+
const type = ctx.typeById.get(String(doc.typeId));
|
|
517
|
+
const values = [
|
|
518
|
+
...valuesFor(doc.fieldValues, fdef, slot),
|
|
519
|
+
...valuesFor(type === null || type === void 0 ? void 0 : type.typeFieldValuesAll, fdef, slot),
|
|
520
|
+
];
|
|
521
|
+
const anyPresent = values.some(isPresent);
|
|
522
|
+
switch (cond.operator) {
|
|
523
|
+
case 'exists':
|
|
524
|
+
return anyPresent;
|
|
525
|
+
case 'notExists':
|
|
526
|
+
return !anyPresent;
|
|
527
|
+
case 'contains': {
|
|
528
|
+
const term = String((_a = cond.value) !== null && _a !== void 0 ? _a : '').toLowerCase();
|
|
529
|
+
return values.some((v) => isPresent(v) && String(v).toLowerCase().includes(term));
|
|
530
|
+
}
|
|
531
|
+
default:
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
// The SAME coercion the Mongo compiler applies — shared, not mirrored.
|
|
535
|
+
// A numeric literal for a text field, or a relative date for a DATE field,
|
|
536
|
+
// has to become one concrete value; two copies of this rule would classify
|
|
537
|
+
// the same document differently.
|
|
538
|
+
const target = (0, validate_1.coerceSemanticValue)(cond.value, fdef.dataType, ctx.now, ctx.tz);
|
|
539
|
+
if (cond.operator === 'ne') {
|
|
540
|
+
// Present-and-different at either level; default also counts fully unset.
|
|
541
|
+
const strict = values.some((v) => isPresent(v) && v !== target);
|
|
542
|
+
return cond.includeUnset === false ? strict : strict || !anyPresent;
|
|
543
|
+
}
|
|
544
|
+
return values.some((v) => cmp(v, cond.operator, target));
|
|
545
|
+
}
|
|
510
546
|
//# sourceMappingURL=evaluate.js.map
|