@bitcobblers/wod-wiki-wql 0.8.39 → 0.10.41
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/index.cjs +346 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -33
- package/dist/index.d.ts +98 -33
- package/dist/index.mjs +344 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -55,7 +55,7 @@ var WQL_CALC_TARGETS = [
|
|
|
55
55
|
var WQL_INTENSITY_TIERS = ["low", "moderate", "high"];
|
|
56
56
|
var WQL_GRAINS = ["summary", "event"];
|
|
57
57
|
var WQL_FIND_TARGETS = ["note", "block", "effort"];
|
|
58
|
-
var
|
|
58
|
+
var WQL_SOURCE_VALUES = ["journal", "collections", "feeds", "all"];
|
|
59
59
|
var WQL_RESULT_PLANES = wodWikiCore.KNOWN_OUTPUT_TYPES;
|
|
60
60
|
var WQL_ROWS_TARGETS = [...WQL_FIND_TARGETS, ...WQL_RESULT_PLANES, "all"];
|
|
61
61
|
var WQL_ROWS_SCOPE_KEYS = ["result", "block", "note"];
|
|
@@ -154,6 +154,7 @@ var DISPLAY_UNIT_RE = /\s+in\s+([a-zA-Z0-9_-]+)\s*$/;
|
|
|
154
154
|
var ROLLUP_RE = /\.rollup\((\d+)?([a-zA-Z]*)\)\s*$/;
|
|
155
155
|
var BY_RE = /\s+by\s+\{([^}]*)\}\s*$/;
|
|
156
156
|
var LAST_RE = /\s+last\s+(\d+)([dw])\s*$/i;
|
|
157
|
+
var FROM_TO_RE = /\s+from\s+(\d{4}-\d{2}-\d{2})(?:\s+to\s+(\d{4}-\d{2}-\d{2}))?\s*$/i;
|
|
157
158
|
var IN_SCOPE_RE = /\s+in\s+(\w+)\s*$/;
|
|
158
159
|
function parseWqlSuffixes(raw) {
|
|
159
160
|
const { primary, where } = splitAtWhere(raw);
|
|
@@ -161,8 +162,8 @@ function parseWqlSuffixes(raw) {
|
|
|
161
162
|
let displayUnit;
|
|
162
163
|
let rollup;
|
|
163
164
|
let groupBy;
|
|
164
|
-
let
|
|
165
|
-
let
|
|
165
|
+
let window;
|
|
166
|
+
let legacyScope;
|
|
166
167
|
const conflicts = [];
|
|
167
168
|
const isFind = text.startsWith("find:");
|
|
168
169
|
const isRows = /^rows(?=[:{]|\s|$)/.test(text);
|
|
@@ -181,18 +182,40 @@ function parseWqlSuffixes(raw) {
|
|
|
181
182
|
);
|
|
182
183
|
}
|
|
183
184
|
};
|
|
185
|
+
const froms = stripRepeated(FROM_TO_RE);
|
|
186
|
+
const lasts = stripRepeated(LAST_RE);
|
|
187
|
+
{
|
|
188
|
+
const windows = [
|
|
189
|
+
...froms.map((m) => ({
|
|
190
|
+
kind: "range",
|
|
191
|
+
start: m[1],
|
|
192
|
+
end: m[2],
|
|
193
|
+
raw: m[0].trim(),
|
|
194
|
+
at: m.index
|
|
195
|
+
})),
|
|
196
|
+
...lasts.map((m) => ({
|
|
197
|
+
kind: "relative",
|
|
198
|
+
size: parseInt(m[1], 10),
|
|
199
|
+
unit: m[2].toLowerCase(),
|
|
200
|
+
raw: m[0].trim(),
|
|
201
|
+
at: m.index
|
|
202
|
+
}))
|
|
203
|
+
].sort((a, b) => a.at - b.at);
|
|
204
|
+
if (windows.length > 1) {
|
|
205
|
+
conflicts.push(
|
|
206
|
+
`Duplicate 'window' clause: '${windows[0].raw}' conflicts with '${windows[windows.length - 1].raw}'`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (windows.length === 1) {
|
|
210
|
+
const w = windows[0];
|
|
211
|
+
window = w.kind === "range" ? { kind: "range", start: w.start, end: w.end, raw: w.raw } : { kind: "relative", size: w.size, unit: w.unit, raw: w.raw };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
184
214
|
if (isFind || isRows) {
|
|
185
|
-
const
|
|
186
|
-
conflictFrom("
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
last = { size: parseInt(m[1], 10), unit: m[2].toLowerCase(), raw: m[0].trim() };
|
|
190
|
-
}
|
|
191
|
-
if (isFind) {
|
|
192
|
-
const scopes = stripRepeated(IN_SCOPE_RE);
|
|
193
|
-
conflictFrom("scope", scopes);
|
|
194
|
-
if (scopes.length) scope = scopes[scopes.length - 1][1];
|
|
195
|
-
} else {
|
|
215
|
+
const scopes = stripRepeated(IN_SCOPE_RE);
|
|
216
|
+
conflictFrom("scope", scopes);
|
|
217
|
+
if (scopes.length) legacyScope = scopes[scopes.length - 1][1];
|
|
218
|
+
if (isRows) {
|
|
196
219
|
const rollups = stripRepeated(ROLLUP_RE);
|
|
197
220
|
conflictFrom(".rollup", rollups);
|
|
198
221
|
if (rollups.length) {
|
|
@@ -226,8 +249,8 @@ function parseWqlSuffixes(raw) {
|
|
|
226
249
|
displayUnit,
|
|
227
250
|
rollup,
|
|
228
251
|
groupBy,
|
|
229
|
-
|
|
230
|
-
|
|
252
|
+
window,
|
|
253
|
+
legacyScope,
|
|
231
254
|
conflicts: conflicts.length ? conflicts : void 0,
|
|
232
255
|
primaryText: text
|
|
233
256
|
};
|
|
@@ -255,7 +278,13 @@ function parseJoinClause(where) {
|
|
|
255
278
|
if (where.trimStart().startsWith("find:")) {
|
|
256
279
|
const fp = parseFindQuery(where);
|
|
257
280
|
if (fp.error) return { error: fp.error };
|
|
258
|
-
|
|
281
|
+
if (fp.window?.kind === "range") {
|
|
282
|
+
return { error: "Range windows are not supported on join halves \u2014 use last <n>d|w" };
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
find: { target: fp.target, filters: fp.filters, last: fp.window?.kind === "relative" ? { size: fp.window.size, unit: fp.window.unit } : void 0 },
|
|
286
|
+
advisories: fp.advisories
|
|
287
|
+
};
|
|
259
288
|
}
|
|
260
289
|
const m = CMP_RE.exec(where.trim());
|
|
261
290
|
if (!m) return { error: cannotParseJoin(where) };
|
|
@@ -272,41 +301,119 @@ function parseJoinClause(where) {
|
|
|
272
301
|
};
|
|
273
302
|
}
|
|
274
303
|
function parseQuery(raw) {
|
|
275
|
-
const
|
|
304
|
+
const norm = normalizeWql(raw);
|
|
305
|
+
const trimmed = norm.query.trimStart();
|
|
306
|
+
let result;
|
|
276
307
|
if (trimmed.startsWith("find:")) {
|
|
277
|
-
|
|
308
|
+
result = parseFindQuery(norm.query);
|
|
309
|
+
} else if (/^rows(?=[:{]|\s|$)/.test(trimmed)) {
|
|
310
|
+
result = parseRowsQuery(norm.query);
|
|
311
|
+
} else {
|
|
312
|
+
result = parseAnalyticsQuery(norm.query);
|
|
278
313
|
}
|
|
279
|
-
if (
|
|
280
|
-
|
|
314
|
+
if (norm.advisories.length) {
|
|
315
|
+
result.advisories = [...result.advisories ?? [], ...norm.advisories];
|
|
281
316
|
}
|
|
282
|
-
|
|
317
|
+
result.raw = raw;
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
function normalizeWql(raw) {
|
|
321
|
+
const advisories = [];
|
|
322
|
+
let text = raw.trim();
|
|
323
|
+
if (/^rows:?\s*[{]/.test(text)) {
|
|
324
|
+
text = text.replace(/^rows:?\s*[{]/, "rows:all{");
|
|
325
|
+
advisories.push("Bare 'rows:{...}' syntax is deprecated; use 'rows:all{...}' instead.");
|
|
326
|
+
}
|
|
327
|
+
const { primary, where } = splitAtWhere(text);
|
|
328
|
+
const isFind = primary.startsWith("find:");
|
|
329
|
+
const isRows = /^rows(?=[:{]|\s|$)/.test(primary);
|
|
330
|
+
if (isFind || isRows) {
|
|
331
|
+
const suffixes = parseWqlSuffixes(primary);
|
|
332
|
+
if (suffixes.legacyScope && !suffixes.conflicts?.length) {
|
|
333
|
+
advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
|
|
334
|
+
const scope = suffixes.legacyScope;
|
|
335
|
+
let head = suffixes.primaryText.trim();
|
|
336
|
+
const braceOpen = head.indexOf("{");
|
|
337
|
+
const braceClose = head.lastIndexOf("}");
|
|
338
|
+
if (braceOpen !== -1 && braceClose !== -1 && braceClose > braceOpen) {
|
|
339
|
+
const beforeBrace = head.slice(0, braceOpen + 1);
|
|
340
|
+
const inside = head.slice(braceOpen + 1, braceClose).trim();
|
|
341
|
+
const afterBrace = head.slice(braceClose);
|
|
342
|
+
const newInside = inside ? `${inside},source:${scope}` : `source:${scope}`;
|
|
343
|
+
head = `${beforeBrace}${newInside}${afterBrace}`;
|
|
344
|
+
} else {
|
|
345
|
+
head = `${head}{source:${scope}}`;
|
|
346
|
+
}
|
|
347
|
+
const parts = [head];
|
|
348
|
+
if (suffixes.groupBy) {
|
|
349
|
+
parts.push(`by {${suffixes.groupBy.join(", ")}}`);
|
|
350
|
+
}
|
|
351
|
+
if (suffixes.rollup) {
|
|
352
|
+
parts.push(`.rollup(${suffixes.rollup.raw})`);
|
|
353
|
+
}
|
|
354
|
+
if (suffixes.window) {
|
|
355
|
+
parts.push(suffixes.window.raw);
|
|
356
|
+
}
|
|
357
|
+
if (where) {
|
|
358
|
+
parts.push(`where ${where}`);
|
|
359
|
+
}
|
|
360
|
+
text = parts.join(" ");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return { query: text, advisories };
|
|
283
364
|
}
|
|
284
365
|
function cannotParseRows(text) {
|
|
285
366
|
return `Cannot parse "${text}". Expected rows:all{result:\u2026|block:\u2026|note:\u2026}, rows:<plane>{\u2026}, or rows:segment{\u2026} last 8w`;
|
|
286
367
|
}
|
|
287
368
|
function validateRowsFilters(filters) {
|
|
288
369
|
const scopeKeys = new Set(WQL_ROWS_SCOPE_KEYS);
|
|
370
|
+
const allowedKeys = /* @__PURE__ */ new Set([...WQL_ROWS_SCOPE_KEYS, "source"]);
|
|
289
371
|
const unsupported = filters.filter(
|
|
290
|
-
(f) => !
|
|
372
|
+
(f) => !allowedKeys.has(f.key) || f.negate || f.values.some((v) => v.wildcard)
|
|
291
373
|
);
|
|
292
374
|
if (unsupported.length > 0) {
|
|
293
|
-
|
|
375
|
+
const allowed = [...WQL_ROWS_SCOPE_KEYS, "source"];
|
|
376
|
+
return `Unsupported rows filter(s): ${unsupported.map((f) => (f.negate ? "!" : "") + f.key).join(", ")}. Rows queries support exact ${allowed.map((k) => `${k}:`).join(", ")} values.`;
|
|
294
377
|
}
|
|
295
378
|
if (!filters.some((f) => scopeKeys.has(f.key))) {
|
|
296
379
|
return `Rows query needs a scope: ${WQL_ROWS_SCOPE_KEYS.map((k) => `${k}:`).join(", ")}.`;
|
|
297
380
|
}
|
|
298
381
|
return void 0;
|
|
299
382
|
}
|
|
383
|
+
function validateSourceFilter(filters) {
|
|
384
|
+
const validSources = new Set(WQL_SOURCE_VALUES);
|
|
385
|
+
for (const f of filters) {
|
|
386
|
+
if (f.key !== "source") continue;
|
|
387
|
+
for (const v of f.values) {
|
|
388
|
+
const val = v.value;
|
|
389
|
+
if (validSources.has(val) || val === "collection" || val === "feed" || val.startsWith("collection:") || val.startsWith("feed:")) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
return `Unknown source "${val}". Try: ${WQL_SOURCE_VALUES.join(", ")} (or collection:<id>, feed:<id>)`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return void 0;
|
|
396
|
+
}
|
|
300
397
|
var BARE_ROWS_RETIRED = 'Bare "rows:" is retired \u2014 name a target: rows:all{\u2026} for every output type, or a plane like rows:segment{\u2026}.';
|
|
301
398
|
function parseRowsQuery(raw) {
|
|
302
399
|
const suffixes = parseWqlSuffixes(raw);
|
|
303
|
-
const { where: whereText,
|
|
400
|
+
const { where: whereText, window: windowSuffix, legacyScope, groupBy, rollup, primaryText } = suffixes;
|
|
401
|
+
const win = toQueryWindow(windowSuffix);
|
|
402
|
+
const advisories = [];
|
|
403
|
+
if (legacyScope) {
|
|
404
|
+
advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
|
|
405
|
+
}
|
|
304
406
|
const result = {
|
|
305
407
|
family: "rows",
|
|
306
408
|
raw,
|
|
307
409
|
filters: [],
|
|
308
|
-
|
|
410
|
+
window: win.window,
|
|
411
|
+
...advisories.length ? { advisories } : {}
|
|
309
412
|
};
|
|
413
|
+
if (win.error) {
|
|
414
|
+
result.error = win.error;
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
310
417
|
if (suffixes.conflicts?.length) {
|
|
311
418
|
result.error = suffixes.conflicts.join("; ");
|
|
312
419
|
return result;
|
|
@@ -349,6 +456,18 @@ function parseRowsQuery(raw) {
|
|
|
349
456
|
}
|
|
350
457
|
if (target !== "all") result.outputType = target;
|
|
351
458
|
result.filters = extractFilters(query, text);
|
|
459
|
+
if (legacyScope) {
|
|
460
|
+
result.filters.push({
|
|
461
|
+
key: "source",
|
|
462
|
+
negate: false,
|
|
463
|
+
values: [{ value: legacyScope, wildcard: false }]
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const sourceError = validateSourceFilter(result.filters);
|
|
467
|
+
if (sourceError) {
|
|
468
|
+
result.error = sourceError;
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
352
471
|
const grainError = retiredGrainRollup(result.filters);
|
|
353
472
|
if (grainError) {
|
|
354
473
|
result.error = grainError;
|
|
@@ -361,6 +480,27 @@ function parseRowsQuery(raw) {
|
|
|
361
480
|
}
|
|
362
481
|
return result;
|
|
363
482
|
}
|
|
483
|
+
function isCivilDate(s) {
|
|
484
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
485
|
+
if (!m) return false;
|
|
486
|
+
const y = Number(m[1]);
|
|
487
|
+
const mo = Number(m[2]);
|
|
488
|
+
const d = Number(m[3]);
|
|
489
|
+
const dt = new Date(y, mo - 1, d);
|
|
490
|
+
return dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d;
|
|
491
|
+
}
|
|
492
|
+
function toQueryWindow(w) {
|
|
493
|
+
if (!w) return {};
|
|
494
|
+
if (w.kind === "relative") {
|
|
495
|
+
return { window: { kind: "relative", size: w.size, unit: w.unit } };
|
|
496
|
+
}
|
|
497
|
+
for (const d of [w.start, w.end]) {
|
|
498
|
+
if (d !== void 0 && !isCivilDate(d)) {
|
|
499
|
+
return { error: `Invalid window date "${d}" \u2014 expected a real calendar date YYYY-MM-DD` };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { window: { kind: "range", start: w.start, end: w.end } };
|
|
503
|
+
}
|
|
364
504
|
function retiredGrainRollup(filters) {
|
|
365
505
|
if (filters.some((f) => f.key === "grain" && f.values.some((v) => v.value === "rollup"))) {
|
|
366
506
|
return "grain:rollup is retired \u2014 rollup grains are never stored; compute them with the .rollup suffix";
|
|
@@ -396,7 +536,8 @@ function extractFilters(query, text) {
|
|
|
396
536
|
}
|
|
397
537
|
function parseAnalyticsQuery(raw) {
|
|
398
538
|
const suffixes = parseWqlSuffixes(raw);
|
|
399
|
-
const { where: whereText, displayUnit, groupBy, rollup, primaryText: text } = suffixes;
|
|
539
|
+
const { where: whereText, displayUnit, groupBy, rollup, window: windowSuffix, primaryText: text } = suffixes;
|
|
540
|
+
const win = toQueryWindow(windowSuffix);
|
|
400
541
|
const base = {
|
|
401
542
|
family: "aggregate",
|
|
402
543
|
raw,
|
|
@@ -405,8 +546,13 @@ function parseAnalyticsQuery(raw) {
|
|
|
405
546
|
filters: [],
|
|
406
547
|
groupBy: groupBy ?? [],
|
|
407
548
|
displayUnit,
|
|
408
|
-
rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0
|
|
549
|
+
rollup: rollup ? { size: rollup.size, unit: rollup.unit } : void 0,
|
|
550
|
+
window: win.window
|
|
409
551
|
};
|
|
552
|
+
if (win.error) {
|
|
553
|
+
base.error = win.error;
|
|
554
|
+
return base;
|
|
555
|
+
}
|
|
410
556
|
if (suffixes.conflicts?.length) {
|
|
411
557
|
base.error = suffixes.conflicts.join("; ");
|
|
412
558
|
return base;
|
|
@@ -451,6 +597,9 @@ function parseAnalyticsQuery(raw) {
|
|
|
451
597
|
base.error = join.error;
|
|
452
598
|
return base;
|
|
453
599
|
}
|
|
600
|
+
if (join.advisories?.length) {
|
|
601
|
+
base.advisories = [...base.advisories ?? [], ...join.advisories];
|
|
602
|
+
}
|
|
454
603
|
if (!join.find) {
|
|
455
604
|
base.error = `Cross-store join on an analytics query must be find:\u2026, got "${whereText}"`;
|
|
456
605
|
return base;
|
|
@@ -460,19 +609,28 @@ function parseAnalyticsQuery(raw) {
|
|
|
460
609
|
return base;
|
|
461
610
|
}
|
|
462
611
|
function cannotParseFind(text) {
|
|
463
|
-
return `Cannot parse "${text}". Expected find:target{filters}
|
|
612
|
+
return `Cannot parse "${text}". Expected find:target{filters} last 8w`;
|
|
464
613
|
}
|
|
465
614
|
function parseFindQuery(raw) {
|
|
466
615
|
const suffixes = parseWqlSuffixes(raw);
|
|
467
|
-
const { where: whereText,
|
|
616
|
+
const { where: whereText, window: windowSuffix, legacyScope, primaryText: text } = suffixes;
|
|
617
|
+
const win = toQueryWindow(windowSuffix);
|
|
618
|
+
const advisories = [];
|
|
619
|
+
if (legacyScope) {
|
|
620
|
+
advisories.push("Legacy 'in <scope>' syntax is deprecated; use 'source:<scope>' filter instead.");
|
|
621
|
+
}
|
|
468
622
|
const result = {
|
|
469
623
|
family: "find",
|
|
470
624
|
raw,
|
|
471
625
|
target: "",
|
|
472
626
|
filters: [],
|
|
473
|
-
|
|
474
|
-
|
|
627
|
+
window: win.window,
|
|
628
|
+
...advisories.length ? { advisories } : {}
|
|
475
629
|
};
|
|
630
|
+
if (win.error) {
|
|
631
|
+
result.error = win.error;
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
476
634
|
if (suffixes.conflicts?.length) {
|
|
477
635
|
result.error = suffixes.conflicts.join("; ");
|
|
478
636
|
return result;
|
|
@@ -505,6 +663,18 @@ function parseFindQuery(raw) {
|
|
|
505
663
|
return result;
|
|
506
664
|
}
|
|
507
665
|
result.filters = extractFilters(query, text);
|
|
666
|
+
if (legacyScope) {
|
|
667
|
+
result.filters.push({
|
|
668
|
+
key: "source",
|
|
669
|
+
negate: false,
|
|
670
|
+
values: [{ value: legacyScope, wildcard: false }]
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const sourceError = validateSourceFilter(result.filters);
|
|
674
|
+
if (sourceError) {
|
|
675
|
+
result.error = sourceError;
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
508
678
|
const findGrainError = retiredGrainRollup(result.filters);
|
|
509
679
|
if (findGrainError) {
|
|
510
680
|
result.error = findGrainError;
|
|
@@ -525,6 +695,64 @@ function parseFindQuery(raw) {
|
|
|
525
695
|
return result;
|
|
526
696
|
}
|
|
527
697
|
|
|
698
|
+
// src/serialize.ts
|
|
699
|
+
function serializeValue(value) {
|
|
700
|
+
if (/^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+)?$/.test(value)) return value;
|
|
701
|
+
return `"${value}"`;
|
|
702
|
+
}
|
|
703
|
+
function serializeFilters(filters) {
|
|
704
|
+
return filters.map((f) => `${f.negate ? "!" : ""}${f.key}:${f.values.map((v) => serializeValue(v.value) + (v.wildcard ? "*" : "")).join("|")}`).join(",");
|
|
705
|
+
}
|
|
706
|
+
function serializeAggHead(agg, metric, filters) {
|
|
707
|
+
return `${agg}:${metric}{${serializeFilters(filters)}}`;
|
|
708
|
+
}
|
|
709
|
+
function serializeAggregateHead(a) {
|
|
710
|
+
return serializeAggHead(a.agg, a.metric, a.filters);
|
|
711
|
+
}
|
|
712
|
+
function serializeFindHalf(target, filters, last) {
|
|
713
|
+
const head = `find:${target}${filters.length ? `{${serializeFilters(filters)}}` : ""}`;
|
|
714
|
+
return last ? `${head} last ${last.size}${last.unit}` : head;
|
|
715
|
+
}
|
|
716
|
+
function serializeMetricHalf(j) {
|
|
717
|
+
return `${serializeAggHead(j.agg, j.metric, j.filters)} ${j.operator} ${j.threshold}`;
|
|
718
|
+
}
|
|
719
|
+
function serializeFindHead(f) {
|
|
720
|
+
return serializeFindHalf(f.target, f.filters);
|
|
721
|
+
}
|
|
722
|
+
function serializeRowsHead(r) {
|
|
723
|
+
return `rows:${r.outputType ?? "all"}{${serializeFilters(r.filters)}}`;
|
|
724
|
+
}
|
|
725
|
+
function serializeWindow(w) {
|
|
726
|
+
if (!w) return "";
|
|
727
|
+
if (w.kind === "relative") return `last ${w.size}${w.unit}`;
|
|
728
|
+
return w.end ? `from ${w.start} to ${w.end}` : `from ${w.start}`;
|
|
729
|
+
}
|
|
730
|
+
function serialize(parsed) {
|
|
731
|
+
if (parsed.error) return parsed.raw;
|
|
732
|
+
if (parsed.family === "aggregate") {
|
|
733
|
+
const a = parsed;
|
|
734
|
+
let text = serializeAggregateHead(a);
|
|
735
|
+
if (a.groupBy.length) text += ` by {${a.groupBy.join(", ")}}`;
|
|
736
|
+
if (a.rollup) text += `.rollup(${a.rollup.size}${a.rollup.unit})`;
|
|
737
|
+
if (a.displayUnit) text += ` in ${a.displayUnit}`;
|
|
738
|
+
const win = serializeWindow(a.window);
|
|
739
|
+
if (win) text += ` ${win}`;
|
|
740
|
+
if (a.join) text += ` where ${serializeFindHalf(a.join.target, a.join.filters, a.join.last)}`;
|
|
741
|
+
return text;
|
|
742
|
+
}
|
|
743
|
+
if (parsed.family === "find") {
|
|
744
|
+
const parts = [serializeFindHead(parsed), serializeWindow(parsed.window)];
|
|
745
|
+
if (parsed.join) parts.push(`where ${serializeMetricHalf(parsed.join)}`);
|
|
746
|
+
return parts.filter(Boolean).join(" ");
|
|
747
|
+
}
|
|
748
|
+
if (parsed.family === "rows") {
|
|
749
|
+
const win = serializeWindow(parsed.window);
|
|
750
|
+
return [serializeRowsHead(parsed), win].filter(Boolean).join(" ");
|
|
751
|
+
}
|
|
752
|
+
const unhandled = parsed;
|
|
753
|
+
return unhandled.raw ?? "";
|
|
754
|
+
}
|
|
755
|
+
|
|
528
756
|
// src/disciplines.ts
|
|
529
757
|
var EFFORT_DISCIPLINES = [
|
|
530
758
|
"bodyweight",
|
|
@@ -629,6 +857,8 @@ function wqlCompletionSource(options_ = {}) {
|
|
|
629
857
|
return options(WQL_GRAINS);
|
|
630
858
|
case "metric":
|
|
631
859
|
return metricOptions();
|
|
860
|
+
case "source":
|
|
861
|
+
return options(WQL_SOURCE_VALUES);
|
|
632
862
|
default:
|
|
633
863
|
return null;
|
|
634
864
|
}
|
|
@@ -1123,7 +1353,10 @@ function staticTagIndexFromBlocks(blocks) {
|
|
|
1123
1353
|
var DAY = 864e5;
|
|
1124
1354
|
var ROWS_CONTENT_PLANES = new Set(WQL_FIND_TARGETS);
|
|
1125
1355
|
function localDateString(ts) {
|
|
1126
|
-
|
|
1356
|
+
const d = new Date(ts);
|
|
1357
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
1358
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
1359
|
+
return `${d.getFullYear()}-${mm}-${dd}`;
|
|
1127
1360
|
}
|
|
1128
1361
|
function catalogOfItem(item) {
|
|
1129
1362
|
if (item.catalog) return item.catalog;
|
|
@@ -1133,9 +1366,13 @@ function catalogOfItem(item) {
|
|
|
1133
1366
|
return clean.split("/")[0];
|
|
1134
1367
|
}
|
|
1135
1368
|
function sourceMatches(sourceId, kind) {
|
|
1369
|
+
if (kind === "all") return true;
|
|
1136
1370
|
if (kind === "journal") return !sourceId || sourceId === "journal";
|
|
1137
|
-
if (kind === "collection" || kind === "
|
|
1138
|
-
return !!sourceId && sourceId.startsWith(
|
|
1371
|
+
if (kind === "collection" || kind === "collections") {
|
|
1372
|
+
return !!sourceId && sourceId.startsWith("collection:");
|
|
1373
|
+
}
|
|
1374
|
+
if (kind === "feed" || kind === "feeds") {
|
|
1375
|
+
return !!sourceId && sourceId.startsWith("feed:");
|
|
1139
1376
|
}
|
|
1140
1377
|
return sourceId === kind;
|
|
1141
1378
|
}
|
|
@@ -1150,12 +1387,26 @@ function applySourceFilter(items, filters) {
|
|
|
1150
1387
|
}
|
|
1151
1388
|
return items;
|
|
1152
1389
|
}
|
|
1153
|
-
function
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1390
|
+
function civilMidnight(iso) {
|
|
1391
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
1392
|
+
return new Date(y, m - 1, d).getTime();
|
|
1393
|
+
}
|
|
1394
|
+
function nextCivilMidnight(iso) {
|
|
1395
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
1396
|
+
return new Date(y, m - 1, d + 1).getTime();
|
|
1397
|
+
}
|
|
1398
|
+
function windowRange(w, anchorNow) {
|
|
1399
|
+
if (!w) return void 0;
|
|
1400
|
+
if (w.kind === "relative") {
|
|
1401
|
+
return { start: (anchorNow ?? Date.now()) - w.size * (w.unit === "w" ? 7 : 1) * DAY, end: Number.MAX_SAFE_INTEGER };
|
|
1158
1402
|
}
|
|
1403
|
+
const start = civilMidnight(w.start);
|
|
1404
|
+
const end = w.end !== void 0 ? nextCivilMidnight(w.end) - 1 : Number.MAX_SAFE_INTEGER;
|
|
1405
|
+
return { start, end };
|
|
1406
|
+
}
|
|
1407
|
+
function effectiveTimeWindow(createdAt, window, range, anchorNow) {
|
|
1408
|
+
const resolved = range ?? windowRange(window, anchorNow);
|
|
1409
|
+
if (resolved) return createdAt >= resolved.start && createdAt <= resolved.end;
|
|
1159
1410
|
return true;
|
|
1160
1411
|
}
|
|
1161
1412
|
function latestActivity(rows) {
|
|
@@ -1165,7 +1416,7 @@ function latestActivity(rows) {
|
|
|
1165
1416
|
}
|
|
1166
1417
|
function windowAnchor(selected, parsed, options2) {
|
|
1167
1418
|
if (options2.anchorNow !== void 0) return options2.anchorNow;
|
|
1168
|
-
if (options2.anchor === "latest-activity" && parsed.
|
|
1419
|
+
if (options2.anchor === "latest-activity" && parsed.window?.kind === "relative") return latestActivity(selected);
|
|
1169
1420
|
return void 0;
|
|
1170
1421
|
}
|
|
1171
1422
|
var defaultEventStore = {
|
|
@@ -1235,9 +1486,11 @@ function matchesFilters(row, filters, noteTags) {
|
|
|
1235
1486
|
const negate = group[0].negate;
|
|
1236
1487
|
const raw = factTagValue(row, key, noteTags);
|
|
1237
1488
|
const rowValues = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
|
|
1238
|
-
const hit =
|
|
1239
|
-
(
|
|
1240
|
-
(
|
|
1489
|
+
const hit = rowValues.some(
|
|
1490
|
+
(value) => group.some(
|
|
1491
|
+
(f) => f.values.some(
|
|
1492
|
+
(a) => a.wildcard ? value.startsWith(a.value) : value === a.value
|
|
1493
|
+
)
|
|
1241
1494
|
)
|
|
1242
1495
|
);
|
|
1243
1496
|
return negate ? !hit : hit;
|
|
@@ -1247,8 +1500,8 @@ function dimValue(row, dim, noteTags) {
|
|
|
1247
1500
|
if (dim === "day") return localDateString(row.timestamp);
|
|
1248
1501
|
if (dim === "week") {
|
|
1249
1502
|
const d = new Date(row.timestamp);
|
|
1250
|
-
const monday = new Date(
|
|
1251
|
-
return
|
|
1503
|
+
const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (d.getDay() + 6) % 7);
|
|
1504
|
+
return localDateString(monday.getTime());
|
|
1252
1505
|
}
|
|
1253
1506
|
if (dim === "session") return row.resultId;
|
|
1254
1507
|
const raw = factTagValue(row, dim, noteTags);
|
|
@@ -1374,9 +1627,9 @@ var QueryService = class {
|
|
|
1374
1627
|
for (const blockContentId of blockIds) collect(await this.store.getEventsByContent(blockContentId));
|
|
1375
1628
|
for (const noteId of noteIds) collect(await this.store.getEventsForNote(noteId));
|
|
1376
1629
|
let groups = [...byResult.entries()].filter(([, rows]) => rows.length > 0);
|
|
1377
|
-
if (parsed.
|
|
1630
|
+
if (parsed.window) {
|
|
1378
1631
|
groups = groups.filter(
|
|
1379
|
-
([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.
|
|
1632
|
+
([, rows]) => effectiveTimeWindow(rows[0].timestamp, parsed.window, void 0, options2.anchorNow)
|
|
1380
1633
|
);
|
|
1381
1634
|
}
|
|
1382
1635
|
groups.sort((a, b) => b[1][0].timestamp - a[1][0].timestamp);
|
|
@@ -1404,19 +1657,13 @@ var QueryService = class {
|
|
|
1404
1657
|
return this.runFindEffort(parsed);
|
|
1405
1658
|
}
|
|
1406
1659
|
let notes = [];
|
|
1407
|
-
|
|
1408
|
-
if (
|
|
1409
|
-
notes = notes.concat(await this.
|
|
1410
|
-
}
|
|
1411
|
-
if (scope === "collections" || scope === "feeds" || scope === "all") {
|
|
1412
|
-
let sNotes = this.staticNoteStore ? await this.staticNoteStore.getAllNotes() : [];
|
|
1413
|
-
if (scope === "collections") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("collection:"));
|
|
1414
|
-
if (scope === "feeds") sNotes = sNotes.filter((n) => n.sourceId?.startsWith("feed:"));
|
|
1415
|
-
notes = notes.concat(sNotes);
|
|
1660
|
+
notes = notes.concat(await this.noteStore.getAllNotes());
|
|
1661
|
+
if (this.staticNoteStore) {
|
|
1662
|
+
notes = notes.concat(await this.staticNoteStore.getAllNotes());
|
|
1416
1663
|
}
|
|
1664
|
+
notes = applySourceFilter(notes, parsed.filters);
|
|
1417
1665
|
const selectedCount = notes.length;
|
|
1418
1666
|
const anchorNow = windowAnchor(notes, parsed, options2);
|
|
1419
|
-
notes = applySourceFilter(notes, parsed.filters);
|
|
1420
1667
|
for (const filter of parsed.filters) {
|
|
1421
1668
|
if (filter.key === "tags" && !filter.negate) {
|
|
1422
1669
|
const matchingIds = /* @__PURE__ */ new Set();
|
|
@@ -1458,8 +1705,8 @@ var QueryService = class {
|
|
|
1458
1705
|
});
|
|
1459
1706
|
}
|
|
1460
1707
|
}
|
|
1461
|
-
if (parsed.
|
|
1462
|
-
notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.
|
|
1708
|
+
if (parsed.window || options2.range) {
|
|
1709
|
+
notes = notes.filter((n) => effectiveTimeWindow(n.createdAt, parsed.window, options2.range, anchorNow));
|
|
1463
1710
|
}
|
|
1464
1711
|
if (parsed.join) {
|
|
1465
1712
|
const joined = await this.applyMetricJoin(parsed, notes, []);
|
|
@@ -1472,19 +1719,13 @@ var QueryService = class {
|
|
|
1472
1719
|
*/
|
|
1473
1720
|
async runFindBlock(parsed, options2 = {}) {
|
|
1474
1721
|
let blocks = [];
|
|
1475
|
-
|
|
1476
|
-
if (
|
|
1477
|
-
blocks = blocks.concat(await this.
|
|
1478
|
-
}
|
|
1479
|
-
if (scope === "collections" || scope === "feeds" || scope === "all") {
|
|
1480
|
-
let sBlocks = this.staticBlockStore ? await this.staticBlockStore.getAllBlocks() : [];
|
|
1481
|
-
if (scope === "collections") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("collection:"));
|
|
1482
|
-
if (scope === "feeds") sBlocks = sBlocks.filter((b) => b.sourceId?.startsWith("feed:"));
|
|
1483
|
-
blocks = blocks.concat(sBlocks);
|
|
1722
|
+
blocks = blocks.concat(await this.blockStore.getAllBlocks());
|
|
1723
|
+
if (this.staticBlockStore) {
|
|
1724
|
+
blocks = blocks.concat(await this.staticBlockStore.getAllBlocks());
|
|
1484
1725
|
}
|
|
1726
|
+
blocks = applySourceFilter(blocks, parsed.filters);
|
|
1485
1727
|
const selectedCount = blocks.length;
|
|
1486
1728
|
const anchorNow = windowAnchor(blocks, parsed, options2);
|
|
1487
|
-
blocks = applySourceFilter(blocks, parsed.filters);
|
|
1488
1729
|
for (const filter of parsed.filters) {
|
|
1489
1730
|
if (filter.key === "text" && !filter.negate) {
|
|
1490
1731
|
const search = filter.values.map((v) => v.value).join(" ").toLowerCase();
|
|
@@ -1520,8 +1761,8 @@ var QueryService = class {
|
|
|
1520
1761
|
blocks = blocks.filter((b) => matchingNoteIds.has(b.noteId));
|
|
1521
1762
|
}
|
|
1522
1763
|
}
|
|
1523
|
-
if (parsed.
|
|
1524
|
-
blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.
|
|
1764
|
+
if (parsed.window || options2.range) {
|
|
1765
|
+
blocks = blocks.filter((b) => effectiveTimeWindow(b.createdAt, parsed.window, options2.range, anchorNow));
|
|
1525
1766
|
}
|
|
1526
1767
|
if (parsed.join) {
|
|
1527
1768
|
const joined = await this.applyMetricJoin(parsed, [], blocks);
|
|
@@ -1574,10 +1815,8 @@ var QueryService = class {
|
|
|
1574
1815
|
};
|
|
1575
1816
|
if (parsed.error) return empty;
|
|
1576
1817
|
if (parsed.join) return this.runJoined(parsed, options2);
|
|
1577
|
-
const
|
|
1578
|
-
const
|
|
1579
|
-
const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
|
|
1580
|
-
const eventRows = windowed ? await this.store.getEventsByTimeRange(start, end) : await this.store.scanAll();
|
|
1818
|
+
const range = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
|
|
1819
|
+
const eventRows = range ? await this.store.getEventsByTimeRange(range.start, range.end) : await this.store.scanAll();
|
|
1581
1820
|
const candidates = eventRows.flatMap(projectEventToFacts).filter((row) => row.metricKey === parsed.metric);
|
|
1582
1821
|
const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
|
|
1583
1822
|
const noteTags = await this.loadNoteTags(candidates, touchesTags);
|
|
@@ -1595,8 +1834,24 @@ var QueryService = class {
|
|
|
1595
1834
|
const timeDim = parsed.groupBy.find((d) => d === "day" || d === "week");
|
|
1596
1835
|
const tagDims = parsed.groupBy.filter((d) => d !== "day" && d !== "week");
|
|
1597
1836
|
const bucketMs = timeDim ? (timeDim === "week" ? 7 : 1) * DAY : parsed.rollup ? parsed.rollup.size * (parsed.rollup.unit === "w" ? 7 : 1) * DAY : null;
|
|
1598
|
-
const
|
|
1837
|
+
const civilDay = (ts) => {
|
|
1838
|
+
const d = new Date(ts);
|
|
1839
|
+
return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
|
|
1840
|
+
};
|
|
1841
|
+
const bucketKey = (ts) => {
|
|
1842
|
+
if (timeDim === "day") return civilDay(ts);
|
|
1843
|
+
if (timeDim === "week") {
|
|
1844
|
+
const d = new Date(ts);
|
|
1845
|
+
return civilDay(ts) - (d.getDay() + 6) % 7;
|
|
1846
|
+
}
|
|
1847
|
+
return bucketMs ? Math.floor(ts / bucketMs) : 0;
|
|
1848
|
+
};
|
|
1599
1849
|
const bucketCount = bucketMs ? new Set(matched.map((p) => bucketKey(p.timestamp))).size : matched.length ? 1 : 0;
|
|
1850
|
+
const civilBucketInstant = (anchor, dim) => {
|
|
1851
|
+
const d = new Date(anchor);
|
|
1852
|
+
const back = dim === "week" ? (d.getDay() + 6) % 7 : 0;
|
|
1853
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate() - back, 12).getTime();
|
|
1854
|
+
};
|
|
1600
1855
|
const { unit: targetUnit, convert: shouldConvert } = resolveDisplayUnit(matched, {
|
|
1601
1856
|
directive: parsed.displayUnit,
|
|
1602
1857
|
preferred: options2.preferredUnit
|
|
@@ -1621,7 +1876,7 @@ var QueryService = class {
|
|
|
1621
1876
|
(m) => toDisplayValue(m.value, m.unit ?? m.metricUnit, shouldConvert ? targetUnit : void 0)
|
|
1622
1877
|
);
|
|
1623
1878
|
return {
|
|
1624
|
-
ts: bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
|
|
1879
|
+
ts: timeDim ? civilBucketInstant(members[0].timestamp, timeDim) : bucketMs ? b * bucketMs + bucketMs / 2 : Math.min(...members.map((m) => m.timestamp)),
|
|
1625
1880
|
value: Math.round(aggregate(values, parsed.agg, members, shouldConvert ? targetUnit : void 0) * 100) / 100
|
|
1626
1881
|
};
|
|
1627
1882
|
});
|
|
@@ -1655,17 +1910,14 @@ var QueryService = class {
|
|
|
1655
1910
|
raw: "",
|
|
1656
1911
|
target: join.target,
|
|
1657
1912
|
filters: join.filters,
|
|
1658
|
-
|
|
1659
|
-
last: join.last
|
|
1913
|
+
window: join.last ? { kind: "relative", size: join.last.size, unit: join.last.unit } : void 0
|
|
1660
1914
|
});
|
|
1661
1915
|
const contentIds = await this.contentIdsFromFindResult(findResult);
|
|
1662
1916
|
if (contentIds.size === 0) return empty;
|
|
1663
1917
|
let facts = await this.deriveMetricFacts(contentIds, parsed.metric);
|
|
1664
|
-
|
|
1665
|
-
if (
|
|
1666
|
-
|
|
1667
|
-
const end = options2.rangeEnd ?? Number.MAX_SAFE_INTEGER;
|
|
1668
|
-
facts = facts.filter((f) => f.timestamp >= start && f.timestamp <= end);
|
|
1918
|
+
const joinRange = options2.rangeStart !== void 0 || options2.rangeEnd !== void 0 ? { start: options2.rangeStart ?? 0, end: options2.rangeEnd ?? Number.MAX_SAFE_INTEGER } : windowRange(parsed.window);
|
|
1919
|
+
if (joinRange) {
|
|
1920
|
+
facts = facts.filter((f) => f.timestamp >= joinRange.start && f.timestamp <= joinRange.end);
|
|
1669
1921
|
}
|
|
1670
1922
|
const touchesTags = parsed.filters.some((f) => f.key === "tags") || parsed.groupBy.includes("tags");
|
|
1671
1923
|
const noteTags = await this.loadNoteTags(facts, touchesTags);
|
|
@@ -2070,13 +2322,13 @@ sum:$metric{} rollup:1w
|
|
|
2070
2322
|
}
|
|
2071
2323
|
|
|
2072
2324
|
// src/version.ts
|
|
2073
|
-
var VERSION = "0.
|
|
2074
|
-
var GIT_SHA = "
|
|
2075
|
-
var BUILD_TIME = "2026-08-
|
|
2325
|
+
var VERSION = "0.10.41";
|
|
2326
|
+
var GIT_SHA = "1b9b771";
|
|
2327
|
+
var BUILD_TIME = "2026-08-26T04:16:38.418Z";
|
|
2076
2328
|
var SEMVER = {
|
|
2077
|
-
version: "0.
|
|
2078
|
-
gitSha: "
|
|
2079
|
-
buildTime: "2026-08-
|
|
2329
|
+
version: "0.10.41",
|
|
2330
|
+
gitSha: "1b9b771",
|
|
2331
|
+
buildTime: "2026-08-26T04:16:38.418Z"
|
|
2080
2332
|
};
|
|
2081
2333
|
|
|
2082
2334
|
exports.BUILD_TIME = BUILD_TIME;
|
|
@@ -2107,8 +2359,8 @@ exports.WQL_RESULT_PLANES = WQL_RESULT_PLANES;
|
|
|
2107
2359
|
exports.WQL_ROLLUP_PERIODS = WQL_ROLLUP_PERIODS;
|
|
2108
2360
|
exports.WQL_ROWS_SCOPE_KEYS = WQL_ROWS_SCOPE_KEYS;
|
|
2109
2361
|
exports.WQL_ROWS_TARGETS = WQL_ROWS_TARGETS;
|
|
2110
|
-
exports.WQL_SCOPES = WQL_SCOPES;
|
|
2111
2362
|
exports.WQL_SOURCES = WQL_SOURCES;
|
|
2363
|
+
exports.WQL_SOURCE_VALUES = WQL_SOURCE_VALUES;
|
|
2112
2364
|
exports.WQL_TAG_KEYS = WQL_TAG_KEYS;
|
|
2113
2365
|
exports.WQL_VIRTUAL_DIMS = WQL_VIRTUAL_DIMS;
|
|
2114
2366
|
exports.buildDashboardDocument = buildDashboardDocument;
|
|
@@ -2130,6 +2382,7 @@ exports.isFindQuery = isFindQuery;
|
|
|
2130
2382
|
exports.isProposedMetric = isProposedMetric;
|
|
2131
2383
|
exports.isRowsQuery = isRowsQuery;
|
|
2132
2384
|
exports.normalizeSummaryFacts = normalizeSummaryFacts;
|
|
2385
|
+
exports.normalizeWql = normalizeWql;
|
|
2133
2386
|
exports.parseDashboardNote = parseDashboardNote;
|
|
2134
2387
|
exports.parseFlatProperties = parseFlatProperties;
|
|
2135
2388
|
exports.parseFrontmatter = parseFrontmatter;
|
|
@@ -2144,6 +2397,7 @@ exports.referencedTokens = referencedTokens;
|
|
|
2144
2397
|
exports.resolveCanonicalMetricKey = resolveCanonicalMetricKey;
|
|
2145
2398
|
exports.resolveDisplayUnit = resolveDisplayUnit;
|
|
2146
2399
|
exports.resolveWidgetType = resolveWidgetType;
|
|
2400
|
+
exports.serialize = serialize;
|
|
2147
2401
|
exports.serializeFrontmatter = serializeFrontmatter;
|
|
2148
2402
|
exports.setDashboardTokenValue = setDashboardTokenValue;
|
|
2149
2403
|
exports.splitAtWhere = splitAtWhere;
|