@fieldia/core 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -5
- package/package.json +1 -1
- package/schema/page.schema.json +266 -7
- package/src/index.d.mts +10 -2
- package/src/index.d.ts +10 -2
- package/src/index.js +33 -2
- package/src/index.js.map +1 -1
- package/src/index.mjs +13 -2
- package/src/index.mjs.map +1 -1
- package/src/lib/format/approval-checks.d.mts +8 -0
- package/src/lib/format/approval-checks.d.ts +8 -0
- package/src/lib/format/approval-checks.js +29 -0
- package/src/lib/format/approval-checks.js.map +1 -0
- package/src/lib/format/approval-checks.mjs +26 -0
- package/src/lib/format/approval-checks.mjs.map +1 -0
- package/src/lib/format/approval.d.mts +20 -0
- package/src/lib/format/approval.d.ts +20 -0
- package/src/lib/format/approval.js +25 -0
- package/src/lib/format/approval.js.map +1 -0
- package/src/lib/format/approval.mjs +22 -0
- package/src/lib/format/approval.mjs.map +1 -0
- package/src/lib/format/assist-checks.d.mts +14 -0
- package/src/lib/format/assist-checks.d.ts +14 -0
- package/src/lib/format/assist-checks.js +68 -0
- package/src/lib/format/assist-checks.js.map +1 -0
- package/src/lib/format/assist-checks.mjs +64 -0
- package/src/lib/format/assist-checks.mjs.map +1 -0
- package/src/lib/format/assist.d.mts +41 -0
- package/src/lib/format/assist.d.ts +41 -0
- package/src/lib/format/assist.js +29 -0
- package/src/lib/format/assist.js.map +1 -0
- package/src/lib/format/assist.mjs +26 -0
- package/src/lib/format/assist.mjs.map +1 -0
- package/src/lib/format/editable-checks.d.mts +8 -0
- package/src/lib/format/editable-checks.d.ts +8 -0
- package/src/lib/format/editable-checks.js +38 -0
- package/src/lib/format/editable-checks.js.map +1 -0
- package/src/lib/format/editable-checks.mjs +35 -0
- package/src/lib/format/editable-checks.mjs.map +1 -0
- package/src/lib/format/field.d.mts +86 -0
- package/src/lib/format/field.d.ts +86 -0
- package/src/lib/format/field.js +56 -10
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +55 -9
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/languages-checks.d.mts +8 -0
- package/src/lib/format/languages-checks.d.ts +8 -0
- package/src/lib/format/languages-checks.js +40 -0
- package/src/lib/format/languages-checks.js.map +1 -0
- package/src/lib/format/languages-checks.mjs +37 -0
- package/src/lib/format/languages-checks.mjs.map +1 -0
- package/src/lib/format/later-checks.d.mts +4 -3
- package/src/lib/format/later-checks.d.ts +4 -3
- package/src/lib/format/later-checks.js +4 -3
- package/src/lib/format/later-checks.js.map +1 -1
- package/src/lib/format/later-checks.mjs +4 -3
- package/src/lib/format/later-checks.mjs.map +1 -1
- package/src/lib/format/layout.d.mts +123 -28
- package/src/lib/format/layout.d.ts +123 -28
- package/src/lib/format/layout.js +20 -4
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +18 -2
- package/src/lib/format/layout.mjs.map +1 -1
- package/src/lib/format/layout.types.d.mts +2 -1
- package/src/lib/format/layout.types.d.ts +2 -1
- package/src/lib/format/list-editable.d.mts +20 -0
- package/src/lib/format/list-editable.d.ts +20 -0
- package/src/lib/format/list-editable.js +33 -0
- package/src/lib/format/list-editable.js.map +1 -0
- package/src/lib/format/list-editable.mjs +28 -0
- package/src/lib/format/list-editable.mjs.map +1 -0
- package/src/lib/format/page.d.mts +91 -9
- package/src/lib/format/page.d.ts +91 -9
- package/src/lib/format/page.js +4 -0
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +4 -0
- package/src/lib/format/page.mjs.map +1 -1
- package/src/lib/format/photos-options.d.mts +29 -0
- package/src/lib/format/photos-options.d.ts +29 -0
- package/src/lib/format/photos-options.js +49 -0
- package/src/lib/format/photos-options.js.map +1 -0
- package/src/lib/format/photos-options.mjs +45 -0
- package/src/lib/format/photos-options.mjs.map +1 -0
- package/src/lib/format/references.js +31 -1
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +31 -1
- package/src/lib/format/references.mjs.map +1 -1
- package/src/lib/format/scan-checks.d.mts +12 -0
- package/src/lib/format/scan-checks.d.ts +12 -0
- package/src/lib/format/scan-checks.js +89 -0
- package/src/lib/format/scan-checks.js.map +1 -0
- package/src/lib/format/scan-checks.mjs +86 -0
- package/src/lib/format/scan-checks.mjs.map +1 -0
- package/src/lib/format/scan-options.d.mts +84 -0
- package/src/lib/format/scan-options.d.ts +84 -0
- package/src/lib/format/scan-options.js +58 -0
- package/src/lib/format/scan-options.js.map +1 -0
- package/src/lib/format/scan-options.mjs +54 -0
- package/src/lib/format/scan-options.mjs.map +1 -0
- package/src/lib/format/upgrade.d.mts +4 -1
- package/src/lib/format/upgrade.d.ts +4 -1
- package/src/lib/format/upgrade.js +4 -1
- package/src/lib/format/upgrade.js.map +1 -1
- package/src/lib/format/upgrade.mjs +4 -1
- package/src/lib/format/upgrade.mjs.map +1 -1
- package/src/lib/format/version.d.mts +2 -2
- package/src/lib/format/version.d.ts +2 -2
- package/src/lib/format/version.js +2 -2
- package/src/lib/format/version.js.map +1 -1
- package/src/lib/format/version.mjs +2 -2
- package/src/lib/format/version.mjs.map +1 -1
- package/src/lib/record/approvals-part.d.mts +6 -0
- package/src/lib/record/approvals-part.d.ts +6 -0
- package/src/lib/record/approvals-part.js +14 -0
- package/src/lib/record/approvals-part.js.map +1 -0
- package/src/lib/record/approvals-part.mjs +7 -0
- package/src/lib/record/approvals-part.mjs.map +1 -0
- package/src/lib/record/approving.d.mts +45 -0
- package/src/lib/record/approving.d.ts +45 -0
- package/src/lib/record/approving.js +10 -0
- package/src/lib/record/approving.js.map +1 -0
- package/src/lib/record/approving.mjs +7 -0
- package/src/lib/record/approving.mjs.map +1 -0
- package/src/lib/record/check.js +7 -1
- package/src/lib/record/check.js.map +1 -1
- package/src/lib/record/check.mjs +7 -1
- package/src/lib/record/check.mjs.map +1 -1
- package/src/lib/record/data-source.d.mts +270 -0
- package/src/lib/record/data-source.d.ts +270 -0
- package/src/lib/record/data-source.js.map +1 -1
- package/src/lib/record/data-source.mjs.map +1 -1
- package/src/lib/record/day-words.d.mts +10 -0
- package/src/lib/record/day-words.d.ts +10 -0
- package/src/lib/record/day-words.js +23 -0
- package/src/lib/record/day-words.js.map +1 -0
- package/src/lib/record/day-words.mjs +20 -0
- package/src/lib/record/day-words.mjs.map +1 -0
- package/src/lib/record/files-part.d.mts +7 -0
- package/src/lib/record/files-part.d.ts +7 -0
- package/src/lib/record/files-part.js +167 -0
- package/src/lib/record/files-part.js.map +1 -0
- package/src/lib/record/files-part.mjs +163 -0
- package/src/lib/record/files-part.mjs.map +1 -0
- package/src/lib/record/filter.d.mts +5 -0
- package/src/lib/record/filter.d.ts +5 -0
- package/src/lib/record/filter.js +1 -0
- package/src/lib/record/filter.js.map +1 -1
- package/src/lib/record/filter.mjs +1 -1
- package/src/lib/record/filter.mjs.map +1 -1
- package/src/lib/record/form-approvals.d.mts +47 -0
- package/src/lib/record/form-approvals.d.ts +47 -0
- package/src/lib/record/form-approvals.js +186 -0
- package/src/lib/record/form-approvals.js.map +1 -0
- package/src/lib/record/form-approvals.mjs +180 -0
- package/src/lib/record/form-approvals.mjs.map +1 -0
- package/src/lib/record/form-limits.d.mts +5 -8
- package/src/lib/record/form-limits.d.ts +5 -8
- package/src/lib/record/form-limits.js +20 -21
- package/src/lib/record/form-limits.js.map +1 -1
- package/src/lib/record/form-limits.mjs +21 -22
- package/src/lib/record/form-limits.mjs.map +1 -1
- package/src/lib/record/form.d.mts +41 -8
- package/src/lib/record/form.d.ts +41 -8
- package/src/lib/record/form.js +119 -11
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +119 -11
- package/src/lib/record/form.mjs.map +1 -1
- package/src/lib/record/languages-part.d.mts +32 -0
- package/src/lib/record/languages-part.d.ts +32 -0
- package/src/lib/record/languages-part.js +104 -0
- package/src/lib/record/languages-part.js.map +1 -0
- package/src/lib/record/languages-part.mjs +96 -0
- package/src/lib/record/languages-part.mjs.map +1 -0
- package/src/lib/record/languages.d.mts +26 -0
- package/src/lib/record/languages.d.ts +26 -0
- package/src/lib/record/languages.js +20 -0
- package/src/lib/record/languages.js.map +1 -0
- package/src/lib/record/languages.mjs +15 -0
- package/src/lib/record/languages.mjs.map +1 -0
- package/src/lib/record/limits.js +4 -2
- package/src/lib/record/limits.js.map +1 -1
- package/src/lib/record/limits.mjs +4 -2
- package/src/lib/record/limits.mjs.map +1 -1
- package/src/lib/record/locales/ar.js +6 -0
- package/src/lib/record/locales/ar.js.map +1 -1
- package/src/lib/record/locales/ar.mjs +6 -0
- package/src/lib/record/locales/ar.mjs.map +1 -1
- package/src/lib/record/locales/de.js +6 -0
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +6 -0
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/locales/fr.js +6 -0
- package/src/lib/record/locales/fr.js.map +1 -1
- package/src/lib/record/locales/fr.mjs +6 -0
- package/src/lib/record/locales/fr.mjs.map +1 -1
- package/src/lib/record/memory-approvals.d.mts +47 -0
- package/src/lib/record/memory-approvals.d.ts +47 -0
- package/src/lib/record/memory-approvals.js +78 -0
- package/src/lib/record/memory-approvals.js.map +1 -0
- package/src/lib/record/memory-approvals.mjs +75 -0
- package/src/lib/record/memory-approvals.mjs.map +1 -0
- package/src/lib/record/memory-assist.d.mts +53 -0
- package/src/lib/record/memory-assist.d.ts +53 -0
- package/src/lib/record/memory-assist.js +519 -0
- package/src/lib/record/memory-assist.js.map +1 -0
- package/src/lib/record/memory-assist.mjs +513 -0
- package/src/lib/record/memory-assist.mjs.map +1 -0
- package/src/lib/record/memory-clusters.d.mts +18 -0
- package/src/lib/record/memory-clusters.d.ts +18 -0
- package/src/lib/record/memory-clusters.js +83 -0
- package/src/lib/record/memory-clusters.js.map +1 -0
- package/src/lib/record/memory-clusters.mjs +80 -0
- package/src/lib/record/memory-clusters.mjs.map +1 -0
- package/src/lib/record/memory-data-source.d.mts +43 -3
- package/src/lib/record/memory-data-source.d.ts +43 -3
- package/src/lib/record/memory-data-source.js +105 -19
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +105 -19
- package/src/lib/record/memory-data-source.mjs.map +1 -1
- package/src/lib/record/memory-later.d.mts +1 -1
- package/src/lib/record/memory-later.d.ts +1 -1
- package/src/lib/record/memory-later.js +4 -1
- package/src/lib/record/memory-later.js.map +1 -1
- package/src/lib/record/memory-later.mjs +4 -1
- package/src/lib/record/memory-later.mjs.map +1 -1
- package/src/lib/record/messages.d.mts +24 -2
- package/src/lib/record/messages.d.ts +24 -2
- package/src/lib/record/messages.js +8 -4
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +8 -4
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/option-rules.d.mts +7 -1
- package/src/lib/record/option-rules.d.ts +7 -1
- package/src/lib/record/option-rules.js +9 -3
- package/src/lib/record/option-rules.js.map +1 -1
- package/src/lib/record/option-rules.mjs +9 -3
- package/src/lib/record/option-rules.mjs.map +1 -1
- package/src/lib/record/record-words.d.mts +13 -1
- package/src/lib/record/record-words.d.ts +13 -1
- package/src/lib/record/record-words.js +29 -1
- package/src/lib/record/record-words.js.map +1 -1
- package/src/lib/record/record-words.mjs +27 -1
- package/src/lib/record/record-words.mjs.map +1 -1
- package/src/lib/record/uploads.d.mts +35 -9
- package/src/lib/record/uploads.d.ts +35 -9
- package/src/lib/record/uploads.js +49 -169
- package/src/lib/record/uploads.js.map +1 -1
- package/src/lib/record/uploads.mjs +42 -164
- package/src/lib/record/uploads.mjs.map +1 -1
- package/src/lib/record/values.d.mts +23 -0
- package/src/lib/record/values.d.ts +23 -0
- package/src/lib/record/values.js +4 -0
- package/src/lib/record/values.js.map +1 -1
- package/src/lib/record/values.mjs +4 -0
- package/src/lib/record/values.mjs.map +1 -1
- package/src/lib/record/waits.d.mts +7 -5
- package/src/lib/record/waits.d.ts +7 -5
- package/src/lib/record/waits.js +7 -10
- package/src/lib/record/waits.js.map +1 -1
- package/src/lib/record/waits.mjs +7 -10
- package/src/lib/record/waits.mjs.map +1 -1
- package/src/runtime.d.mts +13 -6
- package/src/runtime.d.ts +13 -6
- package/src/runtime.js +22 -3
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +11 -2
- package/src/runtime.mjs.map +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gridClusters = gridClusters;
|
|
4
|
+
const filter_1 = require("./filter");
|
|
5
|
+
const values_1 = require("./values");
|
|
6
|
+
/**
|
|
7
|
+
* How a memory source answers `clusters` (its `clusters` option), for demos and tests: as a server gathers a map's
|
|
8
|
+
* records by a grid. The records the search finds in the view, each put in a square of the world as the map draws it
|
|
9
|
+
* at its zoom — `radius` pixels a side, the squares fixed to the world, so a record keeps its square as the map pans —
|
|
10
|
+
* and each square holding two or more one bubble: their middle, their count, the box they lie in (and their `weight`,
|
|
11
|
+
* added up, where asked). A square holding one is that record, standing alone, as a list's row gives it (the memory
|
|
12
|
+
* source's `row`: the fields asked, as this person may read them). Those the search finds with no place are counted
|
|
13
|
+
* (`unplaced`). `models` names the models it gathers; a map of any other is answered `false`, and loads its records as
|
|
14
|
+
* without it. A map that moved on (`signal`) is refused with an AbortError. Outside the one-tag script, as
|
|
15
|
+
* `alikeRecords` is.
|
|
16
|
+
*
|
|
17
|
+
* createMemoryDataSource({ records, clusters: gridClusters({ models: ['home'] }) })
|
|
18
|
+
*/
|
|
19
|
+
function gridClusters(how = {}) {
|
|
20
|
+
return (request, records, row = (id, values) => ({ id, values: Object.fromEntries(request.fields.map((field) => [field, (0, values_1.structuredCopy)(values[field] ?? null)])) })) => {
|
|
21
|
+
if (request.signal?.aborted)
|
|
22
|
+
throw Object.assign(new Error('The map moved on'), { name: 'AbortError' });
|
|
23
|
+
if (how.models && !how.models.includes(request.model))
|
|
24
|
+
return false;
|
|
25
|
+
const world = 256 * 2 ** request.zoom;
|
|
26
|
+
const side = Math.max(1, request.radius);
|
|
27
|
+
const squares = new Map();
|
|
28
|
+
let unplaced = 0;
|
|
29
|
+
for (const [key, values] of Object.entries(records[request.model] ?? {})) {
|
|
30
|
+
const id = Number.isFinite(Number(key)) ? Number(key) : key;
|
|
31
|
+
if (request.filter.length && !(0, filter_1.matchesFilter)({ id, ...values }, request.filter))
|
|
32
|
+
continue;
|
|
33
|
+
const at = (0, filter_1.placeOf)(values, values[request.field], request.longitude);
|
|
34
|
+
if (!at || Math.abs(at.lat) > 90 || Math.abs(at.lng) > 180) {
|
|
35
|
+
unplaced++;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!inBox(at, request.bbox))
|
|
39
|
+
continue;
|
|
40
|
+
// Where it is drawn at this zoom (Web Mercator), in the world's pixels, then its square.
|
|
41
|
+
const sin = Math.min(0.9999, Math.max(-0.9999, Math.sin((at.lat * Math.PI) / 180)));
|
|
42
|
+
const x = ((at.lng + 180) / 360) * world;
|
|
43
|
+
const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * world;
|
|
44
|
+
const name = `${Math.floor(x / side)}:${Math.floor(y / side)}`;
|
|
45
|
+
const square = squares.get(name);
|
|
46
|
+
const weight = request.weight ? Number(values[request.weight]) : 0;
|
|
47
|
+
const weighs = Number.isFinite(weight) ? weight : 0;
|
|
48
|
+
if (!square)
|
|
49
|
+
squares.set(name, { first: { id, values }, count: 1, lat: at.lat, lng: at.lng, box: { south: at.lat, west: at.lng, north: at.lat, east: at.lng }, weight: weighs });
|
|
50
|
+
else {
|
|
51
|
+
square.count++;
|
|
52
|
+
square.lat += at.lat;
|
|
53
|
+
square.lng += at.lng;
|
|
54
|
+
square.weight += weighs;
|
|
55
|
+
square.box = { south: Math.min(square.box.south, at.lat), west: Math.min(square.box.west, at.lng), north: Math.max(square.box.north, at.lat), east: Math.max(square.box.east, at.lng) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const clusters = [];
|
|
59
|
+
const points = [];
|
|
60
|
+
for (const square of squares.values()) {
|
|
61
|
+
if (square.count === 1) {
|
|
62
|
+
points.push(row(square.first.id, square.first.values));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const round = (n) => Math.round(n * 1e6) / 1e6;
|
|
66
|
+
clusters.push({
|
|
67
|
+
lat: round(square.lat / square.count),
|
|
68
|
+
lng: round(square.lng / square.count),
|
|
69
|
+
count: square.count,
|
|
70
|
+
bbox: square.box,
|
|
71
|
+
...(request.weight ? { weight: square.weight } : {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { clusters, points, unplaced };
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Whether a place lies in a box, its edges included: west beyond east crossing the antimeridian. */
|
|
78
|
+
function inBox(at, box) {
|
|
79
|
+
if (at.lat < box.south || at.lat > box.north)
|
|
80
|
+
return false;
|
|
81
|
+
return box.west <= box.east ? at.lng >= box.west && at.lng <= box.east : at.lng >= box.west || at.lng <= box.east;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=memory-clusters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-clusters.js","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/record/memory-clusters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { matchesFilter, placeOf } from './filter.mjs';
|
|
2
|
+
import { structuredCopy } from './values.mjs';
|
|
3
|
+
/**
|
|
4
|
+
* How a memory source answers `clusters` (its `clusters` option), for demos and tests: as a server gathers a map's
|
|
5
|
+
* records by a grid. The records the search finds in the view, each put in a square of the world as the map draws it
|
|
6
|
+
* at its zoom — `radius` pixels a side, the squares fixed to the world, so a record keeps its square as the map pans —
|
|
7
|
+
* and each square holding two or more one bubble: their middle, their count, the box they lie in (and their `weight`,
|
|
8
|
+
* added up, where asked). A square holding one is that record, standing alone, as a list's row gives it (the memory
|
|
9
|
+
* source's `row`: the fields asked, as this person may read them). Those the search finds with no place are counted
|
|
10
|
+
* (`unplaced`). `models` names the models it gathers; a map of any other is answered `false`, and loads its records as
|
|
11
|
+
* without it. A map that moved on (`signal`) is refused with an AbortError. Outside the one-tag script, as
|
|
12
|
+
* `alikeRecords` is.
|
|
13
|
+
*
|
|
14
|
+
* createMemoryDataSource({ records, clusters: gridClusters({ models: ['home'] }) })
|
|
15
|
+
*/
|
|
16
|
+
export function gridClusters(how = {}) {
|
|
17
|
+
return (request, records, row = (id, values) => ({ id, values: Object.fromEntries(request.fields.map((field) => [field, structuredCopy(values[field] ?? null)])) })) => {
|
|
18
|
+
if (request.signal?.aborted)
|
|
19
|
+
throw Object.assign(new Error('The map moved on'), { name: 'AbortError' });
|
|
20
|
+
if (how.models && !how.models.includes(request.model))
|
|
21
|
+
return false;
|
|
22
|
+
const world = 256 * 2 ** request.zoom;
|
|
23
|
+
const side = Math.max(1, request.radius);
|
|
24
|
+
const squares = new Map();
|
|
25
|
+
let unplaced = 0;
|
|
26
|
+
for (const [key, values] of Object.entries(records[request.model] ?? {})) {
|
|
27
|
+
const id = Number.isFinite(Number(key)) ? Number(key) : key;
|
|
28
|
+
if (request.filter.length && !matchesFilter({ id, ...values }, request.filter))
|
|
29
|
+
continue;
|
|
30
|
+
const at = placeOf(values, values[request.field], request.longitude);
|
|
31
|
+
if (!at || Math.abs(at.lat) > 90 || Math.abs(at.lng) > 180) {
|
|
32
|
+
unplaced++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!inBox(at, request.bbox))
|
|
36
|
+
continue;
|
|
37
|
+
// Where it is drawn at this zoom (Web Mercator), in the world's pixels, then its square.
|
|
38
|
+
const sin = Math.min(0.9999, Math.max(-0.9999, Math.sin((at.lat * Math.PI) / 180)));
|
|
39
|
+
const x = ((at.lng + 180) / 360) * world;
|
|
40
|
+
const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * world;
|
|
41
|
+
const name = `${Math.floor(x / side)}:${Math.floor(y / side)}`;
|
|
42
|
+
const square = squares.get(name);
|
|
43
|
+
const weight = request.weight ? Number(values[request.weight]) : 0;
|
|
44
|
+
const weighs = Number.isFinite(weight) ? weight : 0;
|
|
45
|
+
if (!square)
|
|
46
|
+
squares.set(name, { first: { id, values }, count: 1, lat: at.lat, lng: at.lng, box: { south: at.lat, west: at.lng, north: at.lat, east: at.lng }, weight: weighs });
|
|
47
|
+
else {
|
|
48
|
+
square.count++;
|
|
49
|
+
square.lat += at.lat;
|
|
50
|
+
square.lng += at.lng;
|
|
51
|
+
square.weight += weighs;
|
|
52
|
+
square.box = { south: Math.min(square.box.south, at.lat), west: Math.min(square.box.west, at.lng), north: Math.max(square.box.north, at.lat), east: Math.max(square.box.east, at.lng) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const clusters = [];
|
|
56
|
+
const points = [];
|
|
57
|
+
for (const square of squares.values()) {
|
|
58
|
+
if (square.count === 1) {
|
|
59
|
+
points.push(row(square.first.id, square.first.values));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const round = (n) => Math.round(n * 1e6) / 1e6;
|
|
63
|
+
clusters.push({
|
|
64
|
+
lat: round(square.lat / square.count),
|
|
65
|
+
lng: round(square.lng / square.count),
|
|
66
|
+
count: square.count,
|
|
67
|
+
bbox: square.box,
|
|
68
|
+
...(request.weight ? { weight: square.weight } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { clusters, points, unplaced };
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Whether a place lies in a box, its edges included: west beyond east crossing the antimeridian. */
|
|
75
|
+
function inBox(at, box) {
|
|
76
|
+
if (at.lat < box.south || at.lat > box.north)
|
|
77
|
+
return false;
|
|
78
|
+
return box.west <= box.east ? at.lng >= box.west && at.lng <= box.east : at.lng >= box.west || at.lng <= box.east;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=memory-clusters.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-clusters.mjs","sourceRoot":"","sources":["../../../../../../libs/core/src/lib/record/memory-clusters.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Option, PropertyDefinition } from '../format/field.mjs';
|
|
2
|
-
import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest } from './data-source.mjs';
|
|
2
|
+
import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, ListedRecord, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest, ApprovalEntry, SuggestRequest, SuggestResult, RewriteRequest, RewriteResult, SearchToFilterRequest, SearchToFilterResult, ClustersRequest, ClustersResult } from './data-source.mjs';
|
|
3
3
|
import { type Scheduler } from './scheduler.mjs';
|
|
4
4
|
import { type MemoryKept } from './memory-kept.mjs';
|
|
5
|
+
import { type MemoryApprovalRoute } from './memory-approvals.mjs';
|
|
5
6
|
import { type FileValue, type RecordId, type Values } from './values.mjs';
|
|
6
7
|
export interface MemoryDataSourceOptions {
|
|
7
8
|
/** Records by model, then by id. */
|
|
@@ -114,14 +115,52 @@ export interface MemoryDataSourceOptions {
|
|
|
114
115
|
}>;
|
|
115
116
|
/** The link "Continue later" answers with, made from the draft's key — the app's own address with it: none without it. */
|
|
116
117
|
draftLink?: (key: string, request: SaveDraftRequest) => string;
|
|
118
|
+
/**
|
|
119
|
+
* How `approvals` answers, by model: the way a record is approved — its levels, who may approve each, any one or all
|
|
120
|
+
* of them, in turn or together, and whether it is sent for approval now — worked out from the record; null where it
|
|
121
|
+
* needs none (an order under its limit). The source keeps each decision as an approval engine would and works out
|
|
122
|
+
* where each level stands; the person deciding is the form's `user`, matched to an approver by id, else by name.
|
|
123
|
+
* Without it, no record needs approval.
|
|
124
|
+
*/
|
|
125
|
+
approvals?: Record<string, (values: Values, id: RecordId) => MemoryApprovalRoute | null | undefined>;
|
|
126
|
+
/** Each record's approval history to start from, by `model:id`, oldest first: what was decided before. */
|
|
127
|
+
approvalHistory?: Record<string, ApprovalEntry[]>;
|
|
128
|
+
/**
|
|
129
|
+
* The language a text kept in several (`{ ar, en }`) is read in where this source says it as one — a link's name, a
|
|
130
|
+
* list's order — as an app's server reads it in its person's language: their tag ("ar"), the first typed where it has
|
|
131
|
+
* none in it, or where this is left out. A search finds a link by its name in any of its languages.
|
|
132
|
+
*/
|
|
133
|
+
language?: string;
|
|
134
|
+
/**
|
|
135
|
+
* What answers AI help, from the records the source keeps — `assistStandIn()` is a stand-in with no model, reading
|
|
136
|
+
* words by their patterns, for demos and tests. Each member given makes the source's member of that name; without
|
|
137
|
+
* it the source has none, so what asks for it is hidden, as with any source.
|
|
138
|
+
*/
|
|
139
|
+
assist?: MemoryAssist;
|
|
140
|
+
/**
|
|
141
|
+
* How `clusters` answers: a list's map's records gathered where they crowd, worked out from the records the source
|
|
142
|
+
* keeps — `gridClusters()` gathers them by a grid, as a server does — or `false` for the map to load them and gather
|
|
143
|
+
* them itself. `row` gives a record standing alone as a list's row is: the fields asked, as this person may read
|
|
144
|
+
* them. Without it, the source has no `clusters`, and every map gathers its records in the browser.
|
|
145
|
+
*/
|
|
146
|
+
clusters?: (request: ClustersRequest, records: Readonly<Record<string, Record<string, Values>>>, row: (id: RecordId, values: Values) => ListedRecord) => ClustersResult | false | Promise<ClustersResult | false>;
|
|
147
|
+
}
|
|
148
|
+
/** What a memory source's AI help answers with (`MemoryDataSourceOptions.assist`): each given, from its records. */
|
|
149
|
+
export interface MemoryAssist {
|
|
150
|
+
suggest?(request: SuggestRequest, records: Readonly<Record<string, Record<string, Values>>>): SuggestResult | Promise<SuggestResult>;
|
|
151
|
+
rewrite?(request: RewriteRequest, records: Readonly<Record<string, Record<string, Values>>>): RewriteResult | Promise<RewriteResult>;
|
|
152
|
+
searchToFilter?(request: SearchToFilterRequest, records: Readonly<Record<string, Record<string, Values>>>): SearchToFilterResult | Promise<SearchToFilterResult>;
|
|
117
153
|
}
|
|
154
|
+
/** The members of AI help a memory source has only where its `assist` answers them. */
|
|
155
|
+
type AssistMembers = 'suggest' | 'rewrite' | 'searchToFilter';
|
|
118
156
|
/**
|
|
119
157
|
* Every member of a data source but `exportRecords` and `importRecords`: a
|
|
120
158
|
* memory source leaves an export and an import to the browser, as a source
|
|
121
159
|
* without a server of its own does. A test or a demo gives it one by setting
|
|
122
|
-
* it.
|
|
160
|
+
* it. AI help's (`suggest`, `rewrite`, `searchToFilter`) where its `assist`
|
|
161
|
+
* option answers them. `clusters` is there where its option says how it answers.
|
|
123
162
|
*/
|
|
124
|
-
export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords'>>, Pick<DataSource, 'exportRecords' | 'importRecords'> {
|
|
163
|
+
export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'>>, Pick<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'> {
|
|
125
164
|
readonly records: Record<string, Record<string, Values>>;
|
|
126
165
|
/**
|
|
127
166
|
* The records said to be someone else for one another, by model: each pair of ids, in the order kept, through
|
|
@@ -158,3 +197,4 @@ export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecor
|
|
|
158
197
|
export declare function createMemoryDataSource(options?: MemoryDataSourceOptions): MemoryDataSource;
|
|
159
198
|
/** A memory source's members, working on what it keeps (`memoryKept`): the one-tag script's memory part. */
|
|
160
199
|
export declare function memorySource(options: MemoryDataSourceOptions, kept: MemoryKept): MemoryDataSource;
|
|
200
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Option, PropertyDefinition } from '../format/field';
|
|
2
|
-
import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest } from './data-source';
|
|
2
|
+
import type { DataSource, OptionsRequest, SubmitRequest, SubmitResult, ListedRecord, RecordPermissions, RecordVersion, UploadFile, SlotsRequest, SlotsResult, DuplicatesRequest, DuplicatesResult, LimitsResult, StoredDraft, SaveDraftRequest, ApprovalEntry, SuggestRequest, SuggestResult, RewriteRequest, RewriteResult, SearchToFilterRequest, SearchToFilterResult, ClustersRequest, ClustersResult } from './data-source';
|
|
3
3
|
import { type Scheduler } from './scheduler';
|
|
4
4
|
import { type MemoryKept } from './memory-kept';
|
|
5
|
+
import { type MemoryApprovalRoute } from './memory-approvals';
|
|
5
6
|
import { type FileValue, type RecordId, type Values } from './values';
|
|
6
7
|
export interface MemoryDataSourceOptions {
|
|
7
8
|
/** Records by model, then by id. */
|
|
@@ -114,14 +115,52 @@ export interface MemoryDataSourceOptions {
|
|
|
114
115
|
}>;
|
|
115
116
|
/** The link "Continue later" answers with, made from the draft's key — the app's own address with it: none without it. */
|
|
116
117
|
draftLink?: (key: string, request: SaveDraftRequest) => string;
|
|
118
|
+
/**
|
|
119
|
+
* How `approvals` answers, by model: the way a record is approved — its levels, who may approve each, any one or all
|
|
120
|
+
* of them, in turn or together, and whether it is sent for approval now — worked out from the record; null where it
|
|
121
|
+
* needs none (an order under its limit). The source keeps each decision as an approval engine would and works out
|
|
122
|
+
* where each level stands; the person deciding is the form's `user`, matched to an approver by id, else by name.
|
|
123
|
+
* Without it, no record needs approval.
|
|
124
|
+
*/
|
|
125
|
+
approvals?: Record<string, (values: Values, id: RecordId) => MemoryApprovalRoute | null | undefined>;
|
|
126
|
+
/** Each record's approval history to start from, by `model:id`, oldest first: what was decided before. */
|
|
127
|
+
approvalHistory?: Record<string, ApprovalEntry[]>;
|
|
128
|
+
/**
|
|
129
|
+
* The language a text kept in several (`{ ar, en }`) is read in where this source says it as one — a link's name, a
|
|
130
|
+
* list's order — as an app's server reads it in its person's language: their tag ("ar"), the first typed where it has
|
|
131
|
+
* none in it, or where this is left out. A search finds a link by its name in any of its languages.
|
|
132
|
+
*/
|
|
133
|
+
language?: string;
|
|
134
|
+
/**
|
|
135
|
+
* What answers AI help, from the records the source keeps — `assistStandIn()` is a stand-in with no model, reading
|
|
136
|
+
* words by their patterns, for demos and tests. Each member given makes the source's member of that name; without
|
|
137
|
+
* it the source has none, so what asks for it is hidden, as with any source.
|
|
138
|
+
*/
|
|
139
|
+
assist?: MemoryAssist;
|
|
140
|
+
/**
|
|
141
|
+
* How `clusters` answers: a list's map's records gathered where they crowd, worked out from the records the source
|
|
142
|
+
* keeps — `gridClusters()` gathers them by a grid, as a server does — or `false` for the map to load them and gather
|
|
143
|
+
* them itself. `row` gives a record standing alone as a list's row is: the fields asked, as this person may read
|
|
144
|
+
* them. Without it, the source has no `clusters`, and every map gathers its records in the browser.
|
|
145
|
+
*/
|
|
146
|
+
clusters?: (request: ClustersRequest, records: Readonly<Record<string, Record<string, Values>>>, row: (id: RecordId, values: Values) => ListedRecord) => ClustersResult | false | Promise<ClustersResult | false>;
|
|
147
|
+
}
|
|
148
|
+
/** What a memory source's AI help answers with (`MemoryDataSourceOptions.assist`): each given, from its records. */
|
|
149
|
+
export interface MemoryAssist {
|
|
150
|
+
suggest?(request: SuggestRequest, records: Readonly<Record<string, Record<string, Values>>>): SuggestResult | Promise<SuggestResult>;
|
|
151
|
+
rewrite?(request: RewriteRequest, records: Readonly<Record<string, Record<string, Values>>>): RewriteResult | Promise<RewriteResult>;
|
|
152
|
+
searchToFilter?(request: SearchToFilterRequest, records: Readonly<Record<string, Record<string, Values>>>): SearchToFilterResult | Promise<SearchToFilterResult>;
|
|
117
153
|
}
|
|
154
|
+
/** The members of AI help a memory source has only where its `assist` answers them. */
|
|
155
|
+
type AssistMembers = 'suggest' | 'rewrite' | 'searchToFilter';
|
|
118
156
|
/**
|
|
119
157
|
* Every member of a data source but `exportRecords` and `importRecords`: a
|
|
120
158
|
* memory source leaves an export and an import to the browser, as a source
|
|
121
159
|
* without a server of its own does. A test or a demo gives it one by setting
|
|
122
|
-
* it.
|
|
160
|
+
* it. AI help's (`suggest`, `rewrite`, `searchToFilter`) where its `assist`
|
|
161
|
+
* option answers them. `clusters` is there where its option says how it answers.
|
|
123
162
|
*/
|
|
124
|
-
export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords'>>, Pick<DataSource, 'exportRecords' | 'importRecords'> {
|
|
163
|
+
export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'>>, Pick<DataSource, 'exportRecords' | 'importRecords' | AssistMembers | 'clusters'> {
|
|
125
164
|
readonly records: Record<string, Record<string, Values>>;
|
|
126
165
|
/**
|
|
127
166
|
* The records said to be someone else for one another, by model: each pair of ids, in the order kept, through
|
|
@@ -158,3 +197,4 @@ export interface MemoryDataSource extends Required<Omit<DataSource, 'exportRecor
|
|
|
158
197
|
export declare function createMemoryDataSource(options?: MemoryDataSourceOptions): MemoryDataSource;
|
|
159
198
|
/** A memory source's members, working on what it keeps (`memoryKept`): the one-tag script's memory part. */
|
|
160
199
|
export declare function memorySource(options: MemoryDataSourceOptions, kept: MemoryKept): MemoryDataSource;
|
|
200
|
+
export {};
|
|
@@ -8,6 +8,7 @@ const filter_1 = require("./filter");
|
|
|
8
8
|
const periods_1 = require("./periods");
|
|
9
9
|
const scheduler_1 = require("./scheduler");
|
|
10
10
|
const memory_kept_1 = require("./memory-kept");
|
|
11
|
+
const memory_approvals_1 = require("./memory-approvals");
|
|
11
12
|
const values_1 = require("./values");
|
|
12
13
|
/**
|
|
13
14
|
* A data source that keeps everything in memory. It implements the whole
|
|
@@ -21,6 +22,14 @@ function createMemoryDataSource(options = {}) {
|
|
|
21
22
|
function memorySource(options, kept) {
|
|
22
23
|
const { records, notDuplicatePairs, responses, calls, elsewhere, drafts, table, versionOf, moveOn } = kept;
|
|
23
24
|
let draftIds = 0;
|
|
25
|
+
/** A record's approvals, as an approval engine keeps them (memory-approvals.ts). */
|
|
26
|
+
const approving = (0, memory_approvals_1.memoryApprovals)({
|
|
27
|
+
routes: options.approvals,
|
|
28
|
+
history: options.approvalHistory,
|
|
29
|
+
table,
|
|
30
|
+
moved: (model, id, by) => (options.versions ? moveOn(model, id, by) : undefined),
|
|
31
|
+
now: () => new Date().toISOString(),
|
|
32
|
+
});
|
|
24
33
|
/** Properties added in place, by the list and the definitions the app gave for the record they were added to. */
|
|
25
34
|
const addedDefinitions = new Map();
|
|
26
35
|
// A new line's id comes after every line id of the records given (kept from an earlier visit, say), never one of them.
|
|
@@ -46,10 +55,23 @@ function memorySource(options, kept) {
|
|
|
46
55
|
};
|
|
47
56
|
/** A version and permissions, where this source keeps them. */
|
|
48
57
|
const told = (version, can) => ({ ...(version ? { version } : {}), ...(can ? { can } : {}) });
|
|
58
|
+
/** A record as a list's row gives it: the fields asked, as this person may read them, with its version and what they may do. */
|
|
59
|
+
const row = (model, id, values, fields) => {
|
|
60
|
+
const can = permissions(model, id);
|
|
61
|
+
return { id, values: readable(Object.fromEntries(fields.map((field) => [field, (0, values_1.structuredCopy)(values[field] ?? null)])), can), ...told(versionOf(model, id), can) };
|
|
62
|
+
};
|
|
49
63
|
const pause = () => (options.delayMs ? (0, scheduler_1.wait)(options.delayMs, options.scheduler) : Promise.resolve());
|
|
64
|
+
/** A record's name, or its names in each language it is kept in (`{ ar, en }`). */
|
|
65
|
+
const nameOf = (model, id) => table(model)[String(id)]?.[options.labelField?.[model] ?? 'name'];
|
|
50
66
|
const labelOf = (model, id) => {
|
|
51
|
-
const value =
|
|
52
|
-
|
|
67
|
+
const value = nameOf(model, id);
|
|
68
|
+
const name = oneText(value, options.language);
|
|
69
|
+
return typeof name === 'string' ? name : String(id);
|
|
70
|
+
};
|
|
71
|
+
/** Every name a record goes by: its name, in each of its languages. */
|
|
72
|
+
const namesOf = (model, record) => {
|
|
73
|
+
const value = nameOf(model, record.id);
|
|
74
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? Object.values(value).filter((name) => typeof name === 'string') : [record.label];
|
|
53
75
|
};
|
|
54
76
|
/** A linked record with what its model shows besides its name. */
|
|
55
77
|
const shown = (model, record) => {
|
|
@@ -85,7 +107,7 @@ function memorySource(options, kept) {
|
|
|
85
107
|
/** A record as a filter reads it: its values and its own `id`, which a field of its own named id comes before. */
|
|
86
108
|
const withId = (key, values) => ({ id: asId(key), ...values });
|
|
87
109
|
/** The stored records a filter keeps. */
|
|
88
|
-
const matchingRows = (model, filter) => Object.entries(table(model)).filter(([key, values]) => (
|
|
110
|
+
const matchingRows = (model, filter) => Object.entries(table(model)).filter(([key, values]) => passes(withId(key, values), filter)).map(([, values]) => values);
|
|
89
111
|
/** A save, as a backend makes one: refused where the person may not, or over a newer version; else stored, its version moved on. */
|
|
90
112
|
function write(request) {
|
|
91
113
|
if (request.id !== null && table(request.model)[String(request.id)]) {
|
|
@@ -158,7 +180,7 @@ function memorySource(options, kept) {
|
|
|
158
180
|
}
|
|
159
181
|
return counted;
|
|
160
182
|
}
|
|
161
|
-
|
|
183
|
+
const source = {
|
|
162
184
|
records,
|
|
163
185
|
notDuplicatePairs,
|
|
164
186
|
responses,
|
|
@@ -188,6 +210,14 @@ function memorySource(options, kept) {
|
|
|
188
210
|
const link = request.later && options.draftLink ? options.draftLink(key, request) : undefined;
|
|
189
211
|
return { key, ...(link ? { link } : {}) };
|
|
190
212
|
},
|
|
213
|
+
// A map's records gathered as the option says, each record standing alone as a list's row is.
|
|
214
|
+
...(options.clusters && {
|
|
215
|
+
async clusters(request) {
|
|
216
|
+
calls.push({ method: 'clusters', request });
|
|
217
|
+
await pause();
|
|
218
|
+
return options.clusters(request, records, (id, values) => row(request.model, id, values, request.fields));
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
191
221
|
async loadDraft(request) {
|
|
192
222
|
calls.push({ method: 'loadDraft', request });
|
|
193
223
|
await pause();
|
|
@@ -197,6 +227,16 @@ function memorySource(options, kept) {
|
|
|
197
227
|
const { pageId: _page, later: _later, ...draft } = (0, values_1.structuredCopy)(kept);
|
|
198
228
|
return draft;
|
|
199
229
|
},
|
|
230
|
+
async approvals(request) {
|
|
231
|
+
calls.push({ method: 'approvals', request });
|
|
232
|
+
await pause();
|
|
233
|
+
return approving.approvals(request);
|
|
234
|
+
},
|
|
235
|
+
async decide(request) {
|
|
236
|
+
calls.push({ method: 'decide', request });
|
|
237
|
+
await pause();
|
|
238
|
+
return approving.decide(request);
|
|
239
|
+
},
|
|
200
240
|
async load(request) {
|
|
201
241
|
calls.push({ method: 'load', request });
|
|
202
242
|
await pause();
|
|
@@ -279,12 +319,12 @@ function memorySource(options, kept) {
|
|
|
279
319
|
// Found as a person writes it: in any case, with Arabic letters in any of their forms and digits in any script.
|
|
280
320
|
const query = findable(request.query.trim());
|
|
281
321
|
return Object.entries(table(request.model))
|
|
282
|
-
.filter(([key, values]) => (
|
|
322
|
+
.filter(([key, values]) => passes(withId(key, values), request.filter ?? []))
|
|
283
323
|
.map(([key]) => ({ id: asId(key), label: labelOf(request.model, asId(key)) }))
|
|
284
324
|
// By its name, or a tree's record by its whole path: "Portal\Sprint", "Portal / Sprint"; asked exactly, the name or path itself.
|
|
285
325
|
.filter((record) => request.exact
|
|
286
|
-
?
|
|
287
|
-
:
|
|
326
|
+
? namesOf(request.model, record).some((name) => findable(name) === query) || findable(pathWords(request.model, record.id)) === query
|
|
327
|
+
: namesOf(request.model, record).some((name) => findable(name).includes(query)) || findable(pathWords(request.model, record.id)).includes(query))
|
|
288
328
|
.slice(0, request.limit ?? 8)
|
|
289
329
|
.map((record) => shown(request.model, record));
|
|
290
330
|
},
|
|
@@ -292,14 +332,14 @@ function memorySource(options, kept) {
|
|
|
292
332
|
calls.push({ method: 'list', request });
|
|
293
333
|
await pause();
|
|
294
334
|
const matching = Object.entries(table(request.model))
|
|
295
|
-
.filter(([key, values]) => (
|
|
335
|
+
.filter(([key, values]) => passes(withId(key, values), request.filter))
|
|
296
336
|
.map(([key, values]) => ({ id: asId(key), values }));
|
|
297
337
|
matching.sort((a, b) => {
|
|
298
338
|
for (const order of request.sort) {
|
|
299
339
|
// Nearest first: by how far each is, those with no place last.
|
|
300
340
|
const compared = order.near
|
|
301
341
|
? compareValues((0, filter_1.distanceFrom)(a.values, order.field, order.near), (0, filter_1.distanceFrom)(b.values, order.field, order.near))
|
|
302
|
-
: compareValues(a.values[order.field], b.values[order.field]);
|
|
342
|
+
: compareValues(oneText(a.values[order.field], options.language), oneText(b.values[order.field], options.language));
|
|
303
343
|
// Empty last, whichever way the rest goes.
|
|
304
344
|
if (compared.empty)
|
|
305
345
|
return compared.result;
|
|
@@ -310,18 +350,14 @@ function memorySource(options, kept) {
|
|
|
310
350
|
});
|
|
311
351
|
return {
|
|
312
352
|
total: matching.length,
|
|
313
|
-
records: matching.slice(request.offset, request.offset + request.limit).map(({ id, values }) =>
|
|
314
|
-
const can = permissions(request.model, id);
|
|
315
|
-
const shownValues = readable(Object.fromEntries(request.fields.map((field) => [field, (0, values_1.structuredCopy)(values[field] ?? null)])), can);
|
|
316
|
-
return { id, values: shownValues, ...told(versionOf(request.model, id), can) };
|
|
317
|
-
}),
|
|
353
|
+
records: matching.slice(request.offset, request.offset + request.limit).map(({ id, values }) => row(request.model, id, values, request.fields)),
|
|
318
354
|
...(request.allIds ? { allIds: matching.map(({ id }) => id) } : {}),
|
|
319
355
|
};
|
|
320
356
|
},
|
|
321
357
|
async groups(request) {
|
|
322
358
|
calls.push({ method: 'groups', request });
|
|
323
359
|
await pause();
|
|
324
|
-
return groupRows(matchingRows(request.model, request.filter), request.field, request).map(({ rows: _rows, ...group }) => group);
|
|
360
|
+
return groupRows(matchingRows(request.model, request.filter), request.field, request, options.language).map(({ rows: _rows, ...group }) => group);
|
|
325
361
|
},
|
|
326
362
|
async slots(request) {
|
|
327
363
|
calls.push({ method: 'slots', request });
|
|
@@ -398,7 +434,7 @@ function memorySource(options, kept) {
|
|
|
398
434
|
const rows = matchingRows(request.model, request.filter);
|
|
399
435
|
const result = { count: rows.length, ...totalsOver(rows, request) };
|
|
400
436
|
if (request.groupBy)
|
|
401
|
-
result.groups = groupRows(rows, request.groupBy, request).map(({ rows: under, ...group }) => ({ ...group, ...totalsOver(under, request) }));
|
|
437
|
+
result.groups = groupRows(rows, request.groupBy, request, options.language).map(({ rows: under, ...group }) => ({ ...group, ...totalsOver(under, request) }));
|
|
402
438
|
return result;
|
|
403
439
|
},
|
|
404
440
|
async upload(file, request) {
|
|
@@ -525,6 +561,34 @@ function memorySource(options, kept) {
|
|
|
525
561
|
addedDefinitions.set(`${request.list}:${JSON.stringify(own)}`, (0, values_1.structuredCopy)(request.definitions.filter((d) => !own.some((o) => o.name === d.name))));
|
|
526
562
|
},
|
|
527
563
|
};
|
|
564
|
+
// AI help's members, where the app gave what answers them (`assist`): each called as a server would be, after the
|
|
565
|
+
// source's pause, from the records it keeps.
|
|
566
|
+
const assist = options.assist;
|
|
567
|
+
if (assist?.suggest) {
|
|
568
|
+
const answer = assist.suggest.bind(assist);
|
|
569
|
+
source.suggest = async (request) => {
|
|
570
|
+
calls.push({ method: 'suggest', request });
|
|
571
|
+
await pause();
|
|
572
|
+
return answer(request, records);
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (assist?.rewrite) {
|
|
576
|
+
const answer = assist.rewrite.bind(assist);
|
|
577
|
+
source.rewrite = async (request) => {
|
|
578
|
+
calls.push({ method: 'rewrite', request });
|
|
579
|
+
await pause();
|
|
580
|
+
return answer(request, records);
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (assist?.searchToFilter) {
|
|
584
|
+
const answer = assist.searchToFilter.bind(assist);
|
|
585
|
+
source.searchToFilter = async (request) => {
|
|
586
|
+
calls.push({ method: 'searchToFilter', request });
|
|
587
|
+
await pause();
|
|
588
|
+
return answer(request, records);
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return source;
|
|
528
592
|
}
|
|
529
593
|
/** The highest numeric id of a line among the records given — their tables of lines, and lines inside lines — or 0. */
|
|
530
594
|
function highestLineId(records) {
|
|
@@ -558,6 +622,27 @@ function pathOf(value) {
|
|
|
558
622
|
const kept = names?.filter((name) => typeof name === 'string' && name.trim() !== '');
|
|
559
623
|
return kept?.length ? kept : null;
|
|
560
624
|
}
|
|
625
|
+
/** A text kept in several languages: texts keyed by their languages' tags, { ar, en }. */
|
|
626
|
+
const inLanguages = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0 && Object.entries(value).every(([tag, text]) => typeof text === 'string' && /^[a-z]{2,3}(-[a-z\d]{2,8})*$/i.test(tag));
|
|
627
|
+
/**
|
|
628
|
+
* Whether a record passes a filter (`matchesFilter`), a text it keeps in several languages read in each of them: the
|
|
629
|
+
* record as it reads in one of its languages passes — "villa" finds { ar: "فيلا", en: "Villa" }, "فيلا" too — as a
|
|
630
|
+
* server's search finds a text in any language it keeps.
|
|
631
|
+
*/
|
|
632
|
+
function passes(values, filter) {
|
|
633
|
+
const texts = Object.entries(values).filter((entry) => inLanguages(entry[1]));
|
|
634
|
+
if (!texts.length)
|
|
635
|
+
return (0, filter_1.matchesFilter)(values, filter);
|
|
636
|
+
const tags = new Set(texts.flatMap(([, value]) => Object.keys(value)));
|
|
637
|
+
return [...tags].some((tag) => (0, filter_1.matchesFilter)({ ...values, ...Object.fromEntries(texts.map(([name, value]) => [name, value[tag] ?? null])) }, filter));
|
|
638
|
+
}
|
|
639
|
+
/** A text kept in several languages ({ ar, en }) as one, in `language` (the first typed where it has none there); any other value as it is. */
|
|
640
|
+
function oneText(value, language = '') {
|
|
641
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || 'id' in value || !Object.values(value).every((text) => typeof text === 'string'))
|
|
642
|
+
return value;
|
|
643
|
+
const texts = value;
|
|
644
|
+
return texts[language]?.trim() ? texts[language] : (Object.values(texts).find((text) => text.trim()) ?? null);
|
|
645
|
+
}
|
|
561
646
|
/** Two values in order: a link by its label, text by the reader's rules, numbers by size; `empty` says one of them had nothing. */
|
|
562
647
|
function compareValues(a, b) {
|
|
563
648
|
const blank = (v) => v === null || v === undefined || v === '';
|
|
@@ -570,15 +655,16 @@ function compareValues(a, b) {
|
|
|
570
655
|
return { result: String(x).localeCompare(String(y)), empty: false };
|
|
571
656
|
}
|
|
572
657
|
/**
|
|
573
|
-
* Records under each value of a field: a link's by its record, a record of links (a many2many) under each of its
|
|
658
|
+
* Records under each value of a field (a text kept in several languages by its text in `language`): a link's by its record, a record of links (a many2many) under each of its
|
|
574
659
|
* links, as a server groups tags; nothing as its own group, last; the rest by their labels. With `interval`, a date's
|
|
575
660
|
* by the span it falls in, its first day the group's value and label, in time order.
|
|
576
661
|
*/
|
|
577
|
-
function groupRows(rows, field, by = {}) {
|
|
662
|
+
function groupRows(rows, field, by = {}, language) {
|
|
578
663
|
const grouped = new Map();
|
|
579
664
|
const isLink = (item) => item !== null && typeof item === 'object' && !Array.isArray(item) && 'id' in item;
|
|
580
665
|
for (const values of rows) {
|
|
581
|
-
|
|
666
|
+
// A text kept in several languages, grouped by its text in the source's language.
|
|
667
|
+
const held = (inLanguages(values[field]) ? oneText(values[field], language) : values[field]) ?? null;
|
|
582
668
|
// Links: the record under each one; none, under nothing.
|
|
583
669
|
const each = Array.isArray(held) && !by.interval && held.every(isLink) ? (held.length ? held : [null]) : [held];
|
|
584
670
|
for (const raw of each) {
|