@bandeira-tech/b3nd-save 0.8.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/clients/save-client.d.ts +63 -29
- package/esm/clients/save-client.d.ts.map +1 -1
- package/esm/clients/save-client.js +148 -68
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.d.ts +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.d.ts +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.js +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts +6 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.js +12 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.d.ts +3 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/network/policies/path-vector.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.js +3 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.d.ts +13 -5
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/connection.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.js +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.d.ts +3 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/rig.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.js +46 -4
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.d.ts +4 -4
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.d.ts +119 -80
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.d.ts.map +1 -0
- package/esm/dispatch.d.ts +116 -3
- package/esm/dispatch.d.ts.map +1 -1
- package/esm/dispatch.js +381 -5
- package/esm/elasticsearch/store.d.ts +124 -25
- package/esm/elasticsearch/store.d.ts.map +1 -1
- package/esm/elasticsearch/store.js +451 -92
- package/esm/entity-store.d.ts +1 -1
- package/esm/errors.d.ts +1 -1
- package/esm/errors.js +1 -1
- package/esm/fs/mod.d.ts +26 -0
- package/esm/fs/mod.d.ts.map +1 -1
- package/esm/fs/store.d.ts +100 -23
- package/esm/fs/store.d.ts.map +1 -1
- package/esm/fs/store.js +250 -66
- package/esm/glob.d.ts +114 -0
- package/esm/glob.d.ts.map +1 -0
- package/esm/glob.js +352 -0
- package/esm/identifiers.d.ts +18 -0
- package/esm/identifiers.d.ts.map +1 -0
- package/esm/identifiers.js +19 -0
- package/esm/indexeddb/fields.d.ts +32 -0
- package/esm/indexeddb/fields.d.ts.map +1 -0
- package/esm/indexeddb/fields.js +51 -0
- package/esm/indexeddb/store.d.ts +72 -17
- package/esm/indexeddb/store.d.ts.map +1 -1
- package/esm/indexeddb/store.js +362 -106
- package/esm/ipfs/mod.d.ts +19 -0
- package/esm/ipfs/mod.d.ts.map +1 -1
- package/esm/ipfs/mod.js +19 -0
- package/esm/ipfs/store.d.ts +82 -21
- package/esm/ipfs/store.d.ts.map +1 -1
- package/esm/ipfs/store.js +269 -68
- package/esm/json-fields.d.ts +61 -0
- package/esm/json-fields.d.ts.map +1 -0
- package/esm/json-fields.js +121 -0
- package/esm/localstorage/store.d.ts +89 -21
- package/esm/localstorage/store.d.ts.map +1 -1
- package/esm/localstorage/store.js +247 -54
- package/esm/memory/store.d.ts +35 -4
- package/esm/memory/store.d.ts.map +1 -1
- package/esm/memory/store.js +69 -47
- package/esm/mod.d.ts +2 -0
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +1 -0
- package/esm/mongo/fields.d.ts +53 -0
- package/esm/mongo/fields.d.ts.map +1 -0
- package/esm/mongo/fields.js +71 -0
- package/esm/mongo/mod.d.ts +19 -4
- package/esm/mongo/mod.d.ts.map +1 -1
- package/esm/mongo/mod.js +4 -2
- package/esm/mongo/store.d.ts +114 -25
- package/esm/mongo/store.d.ts.map +1 -1
- package/esm/mongo/store.js +355 -102
- package/esm/postgres/store.d.ts +50 -1
- package/esm/postgres/store.d.ts.map +1 -1
- package/esm/postgres/store.js +133 -13
- package/esm/read.d.ts +54 -7
- package/esm/read.d.ts.map +1 -1
- package/esm/read.js +141 -16
- package/esm/s3/store.d.ts +125 -23
- package/esm/s3/store.d.ts.map +1 -1
- package/esm/s3/store.js +313 -57
- package/esm/sqlite/columns.d.ts +33 -0
- package/esm/sqlite/columns.d.ts.map +1 -0
- package/esm/sqlite/columns.js +51 -0
- package/esm/sqlite/store.d.ts +73 -25
- package/esm/sqlite/store.d.ts.map +1 -1
- package/esm/sqlite/store.js +411 -107
- package/esm/types.d.ts +1 -1
- package/esm/url.d.ts +122 -19
- package/esm/url.d.ts.map +1 -1
- package/esm/url.js +218 -24
- package/esm/walk-via-ls.d.ts +122 -0
- package/esm/walk-via-ls.d.ts.map +1 -0
- package/esm/walk-via-ls.js +178 -0
- package/package.json +1 -1
- package/esm/byte-entity-shim.d.ts +0 -47
- package/esm/byte-entity-shim.d.ts.map +0 -1
- package/esm/byte-entity-shim.js +0 -138
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +0 -1
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.js +0 -0
|
@@ -1,49 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ElasticsearchStore — Elasticsearch implementation of
|
|
2
|
+
* ElasticsearchStore — Elasticsearch implementation of `EntityStore`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* path as the document `_id`. Payload bytes are base64-encoded into a
|
|
7
|
-
* string field — ES has no native binary type that round-trips
|
|
8
|
-
* arbitrary bytes through `_source`.
|
|
4
|
+
* One backend, many entities. Per-entity index naming partitions
|
|
5
|
+
* documents in a way that keeps queries simple:
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* - `BYTES_ENTITY` → index `{indexPrefix}_{protocol}_{hostname}`,
|
|
8
|
+
* docId = uri path. Document `_source` carries `payload` (base64)
|
|
9
|
+
* and a mirrored `path` field that `ls`/`count` regex-queries hit
|
|
10
|
+
* (ES 8 disallows regex on `_id`). This is the original layout —
|
|
11
|
+
* no migration.
|
|
12
|
+
* - any other schema → index
|
|
13
|
+
* `{indexPrefix}_{entityName}_{protocol}_{hostname}`, same docId
|
|
14
|
+
* convention. Document `_source` carries the record fields encoded
|
|
15
|
+
* per canonical `TYPE_TAGS` (bytes → base64, bigint → string,
|
|
16
|
+
* timestamp → ISO-8601 — see `./fields.ts`) plus the mirrored
|
|
17
|
+
* `path` field.
|
|
18
|
+
*
|
|
19
|
+
* ## Lifecycle
|
|
20
|
+
*
|
|
21
|
+
* `entitySupport(schema)` is pure — it plans the field set and computes
|
|
22
|
+
* a collision signature. `provisionEntity(meta)` writes a tiny meta
|
|
23
|
+
* doc to `{indexPrefix}__meta__/entities` keyed by entity name, body
|
|
24
|
+
* `{signature}`; subsequent `entityStatus(meta)` reads it back. The
|
|
25
|
+
* meta doc is also used by `status()` to list provisioned entities.
|
|
26
|
+
*
|
|
27
|
+
* ## Read params
|
|
28
|
+
*
|
|
29
|
+
* `fn=ls` / `fn=count` push down to an ES regex query against the
|
|
30
|
+
* mirrored `path.keyword` field with the shallow-direct-leaves
|
|
31
|
+
* predicate `<docPrefix>[^/]+`. Standard params (`limit`/`page`/
|
|
32
|
+
* `sortBy=uri`/`sortOrder`/`format`) translate to `size`/`from`/`sort`,
|
|
33
|
+
* with `_source: false` for the `format=uris` fast path.
|
|
34
|
+
*
|
|
35
|
+
* `fn=find` (v2 §3.3) reuses the regex query shape but composes the
|
|
36
|
+
* body from `saveGlobToRegexBody(pattern)` directly — `**` → `.*`
|
|
37
|
+
* (crosses `/`), so the same query that backs `ls` walks the entire
|
|
38
|
+
* subtree. The query stays prefix-anchored on `path.keyword` (a
|
|
39
|
+
* `keyword` field with a term dictionary), which Lucene can serve via
|
|
40
|
+
* its `RegexpQuery` using DFA traversal of the term dictionary;
|
|
41
|
+
* prefix-anchored regexes are the fast case (the engine restricts the
|
|
42
|
+
* scan to terms whose common prefix matches the regex's required
|
|
43
|
+
* prefix). The default `[^/]+` shallow body that `_leafQuery` falls
|
|
44
|
+
* back to when no pattern is set is NOT reachable from the find path:
|
|
45
|
+
* `parseUrl` guarantees `params.pattern` is populated whenever
|
|
46
|
+
* `fn=find` dispatches here (URI-embedded glob or the dual-accept
|
|
47
|
+
* `?pattern=` form).
|
|
48
|
+
*
|
|
49
|
+
* `pushDownCount: true` — ES has a native `_count` API that runs the
|
|
50
|
+
* same query shape without scoring or document materialisation. Deep
|
|
51
|
+
* counts (URLs containing `**`) route directly to `_count` with the
|
|
52
|
+
* `saveGlobToRegexBody`-composed regex; dispatch never walks via
|
|
53
|
+
* `handlers.find` to count.
|
|
14
54
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.js";
|
|
55
|
+
import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.js";
|
|
17
56
|
import { dispatchRead } from "../dispatch.js";
|
|
18
57
|
import { storageFailure } from "../errors.js";
|
|
19
58
|
import { toBytes } from "../payload.js";
|
|
20
59
|
import { validateReadParams } from "../read.js";
|
|
60
|
+
import { saveGlobToRegexBody } from "../glob.js";
|
|
61
|
+
import { BYTES_ENTITY, } from "../entity.js";
|
|
62
|
+
import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
|
|
63
|
+
import { computeSignature, decodeRecord, encodeRecord, planFields, } from "../json-fields.js";
|
|
21
64
|
const STORE_NAME = "ElasticsearchStore";
|
|
65
|
+
const META_INDEX_SUFFIX = "__meta__";
|
|
22
66
|
/** Escape characters that are special in Lucene regex syntax. */
|
|
23
67
|
function escapeLuceneRegex(input) {
|
|
24
68
|
return input.replace(/[.?+*|{}\[\]()"\\#@&<>~]/g, "\\$&");
|
|
25
69
|
}
|
|
26
70
|
/**
|
|
27
|
-
* Parse a URI into
|
|
28
|
-
*
|
|
29
|
-
* docId: `path` (without leading slash).
|
|
71
|
+
* Parse a URI into the `(protocol, hostname, docId)` tuple. ES index
|
|
72
|
+
* naming gets layered on top by each entity meta.
|
|
30
73
|
*/
|
|
31
|
-
function
|
|
74
|
+
function uriParts(uri) {
|
|
32
75
|
const url = new URL(uri);
|
|
33
|
-
const protocol = url.protocol.replace(":", "");
|
|
34
|
-
const hostname = url.hostname;
|
|
35
76
|
return {
|
|
36
|
-
|
|
77
|
+
protocol: url.protocol.replace(":", ""),
|
|
78
|
+
hostname: url.hostname,
|
|
37
79
|
docId: url.pathname.substring(1),
|
|
38
80
|
};
|
|
39
81
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
82
|
+
function isBytesSchema(schema) {
|
|
83
|
+
return schema.name === BYTES_ENTITY.name;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Pick the right ES field name to sort on for a declared user field.
|
|
87
|
+
* Any string-encoded value (strings, base64-encoded bytes, decimal
|
|
88
|
+
* bigint strings, ISO timestamps) hits the dynamic `.keyword` subfield
|
|
89
|
+
* since ES 8+ disables fielddata on `text` by default; native numeric
|
|
90
|
+
* and boolean fields sort on the field directly; `json` fields aren't
|
|
91
|
+
* sortable, so we return `null` and let dispatch's post-sort fallback
|
|
92
|
+
* handle them.
|
|
93
|
+
*/
|
|
94
|
+
function esSortField(meta, fieldName) {
|
|
95
|
+
const plan = meta.fields.find((f) => f.name === fieldName);
|
|
96
|
+
if (!plan)
|
|
97
|
+
return null;
|
|
98
|
+
switch (plan.tag) {
|
|
99
|
+
case "string":
|
|
100
|
+
case "bytes":
|
|
101
|
+
case "bigint":
|
|
102
|
+
case "timestamp":
|
|
103
|
+
return `${fieldName}.keyword`;
|
|
104
|
+
case "number":
|
|
105
|
+
case "boolean":
|
|
106
|
+
return fieldName;
|
|
107
|
+
default:
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
47
110
|
}
|
|
48
111
|
export class ElasticsearchStore {
|
|
49
112
|
indexPrefix;
|
|
@@ -56,87 +119,292 @@ export class ElasticsearchStore {
|
|
|
56
119
|
this.indexPrefix = indexPrefix;
|
|
57
120
|
this.executor = executor;
|
|
58
121
|
}
|
|
59
|
-
// ──
|
|
122
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
60
123
|
entitySupport(schema) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
if (isBytesSchema(schema)) {
|
|
125
|
+
const fields = [{ name: "payload", tag: "bytes" }];
|
|
126
|
+
return {
|
|
127
|
+
support: {
|
|
128
|
+
entity: schema.name,
|
|
129
|
+
supported: ["payload"],
|
|
130
|
+
unsupported: [],
|
|
131
|
+
},
|
|
132
|
+
indexInfix: "",
|
|
133
|
+
isBytes: true,
|
|
134
|
+
fields,
|
|
135
|
+
declared: new Set(["payload"]),
|
|
136
|
+
bytesFields: new Set(["payload"]),
|
|
137
|
+
signature: computeSignature(schema.name, fields),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!isValidIdentifier(schema.name)) {
|
|
141
|
+
throw new Error(`${STORE_NAME}: entity name '${schema.name}' must match ${IDENTIFIER_PATTERN.source}`);
|
|
142
|
+
}
|
|
143
|
+
const { fields, unsupported } = planFields(schema.fields);
|
|
144
|
+
const declared = new Set(fields.map((f) => f.name));
|
|
145
|
+
const bytesFields = new Set(fields.filter((f) => f.tag === "bytes").map((f) => f.name));
|
|
146
|
+
return {
|
|
147
|
+
support: {
|
|
148
|
+
entity: schema.name,
|
|
149
|
+
supported: fields.map((f) => f.name),
|
|
150
|
+
unsupported,
|
|
151
|
+
},
|
|
152
|
+
indexInfix: `${schema.name}_`,
|
|
153
|
+
isBytes: false,
|
|
154
|
+
fields,
|
|
155
|
+
declared,
|
|
156
|
+
bytesFields,
|
|
157
|
+
signature: computeSignature(schema.name, fields),
|
|
158
|
+
};
|
|
71
159
|
}
|
|
72
|
-
|
|
73
|
-
|
|
160
|
+
async entityStatus(meta) {
|
|
161
|
+
const recorded = await this._readMetaSignature(meta.support.entity);
|
|
162
|
+
if (recorded === null)
|
|
163
|
+
return "unprovisioned";
|
|
164
|
+
return recorded === meta.signature ? "live" : "unprovisioned";
|
|
74
165
|
}
|
|
75
|
-
|
|
76
|
-
|
|
166
|
+
async provisionEntity(meta) {
|
|
167
|
+
const recorded = await this._readMetaSignature(meta.support.entity);
|
|
168
|
+
if (recorded !== null) {
|
|
169
|
+
if (recorded !== meta.signature) {
|
|
170
|
+
throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
|
|
171
|
+
`provisioned with a different shape`);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await this.executor.index(this._metaIndex(), meta.support.entity, { signature: meta.signature });
|
|
77
176
|
}
|
|
78
|
-
// ──
|
|
79
|
-
async
|
|
177
|
+
// ── Write ────────────────────────────────────────────────────────
|
|
178
|
+
async write(meta, entries) {
|
|
179
|
+
if (entries.length === 0)
|
|
180
|
+
return [];
|
|
181
|
+
if ((await this._readMetaSignature(meta.support.entity)) !== meta.signature) {
|
|
182
|
+
return entries.map(({ uri }) => ({
|
|
183
|
+
success: false,
|
|
184
|
+
...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
80
187
|
const results = [];
|
|
81
|
-
for (const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// field. ES's default dynamic mapping for a string source
|
|
88
|
-
// field produces `path` (text) + `path.keyword` (keyword);
|
|
89
|
-
// we target `path.keyword` for exact-match regex push-down.
|
|
90
|
-
const bytes = await toBytes(entry.payload);
|
|
91
|
-
await this.executor.index(index, docId, {
|
|
92
|
-
payload: encodeBase64(bytes),
|
|
93
|
-
path: docId,
|
|
188
|
+
for (const { uri, record } of entries) {
|
|
189
|
+
const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
|
|
190
|
+
if (extras.length > 0) {
|
|
191
|
+
results.push({
|
|
192
|
+
success: false,
|
|
193
|
+
...storageFailure(new Error(`record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
|
|
94
194
|
});
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const { protocol, hostname, docId } = uriParts(uri);
|
|
199
|
+
const index = this._dataIndex(meta, protocol, hostname);
|
|
200
|
+
let body;
|
|
201
|
+
if (meta.isBytes) {
|
|
202
|
+
const payload = record.payload;
|
|
203
|
+
if (!(payload instanceof Uint8Array) &&
|
|
204
|
+
!(payload instanceof ReadableStream)) {
|
|
205
|
+
throw new Error("BYTES_ENTITY record.payload must be Uint8Array or ReadableStream");
|
|
206
|
+
}
|
|
207
|
+
const bytes = await toBytes(payload);
|
|
208
|
+
body = { payload: encodeBase64(bytes), path: docId };
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const normalised = await this._normaliseBytesFields(record, meta.bytesFields);
|
|
212
|
+
body = {
|
|
213
|
+
...encodeRecord(meta.fields, normalised),
|
|
214
|
+
path: docId,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
await this.executor.index(index, docId, body);
|
|
95
218
|
results.push({ success: true });
|
|
96
219
|
}
|
|
97
220
|
catch (err) {
|
|
98
221
|
results.push({
|
|
99
222
|
success: false,
|
|
100
|
-
...storageFailure(err, "Write failed",
|
|
223
|
+
...storageFailure(err, "Write failed", uri),
|
|
101
224
|
});
|
|
102
225
|
}
|
|
103
226
|
}
|
|
104
227
|
return results;
|
|
105
228
|
}
|
|
106
|
-
|
|
229
|
+
// ── Read ─────────────────────────────────────────────────────────
|
|
230
|
+
read(meta, urls) {
|
|
107
231
|
return dispatchRead(urls, STORE_NAME, {
|
|
108
|
-
read: (p) => this._readOne(p.uri),
|
|
109
|
-
ls: (p) => this._ls(p),
|
|
110
|
-
count: (p) => this._count(p),
|
|
232
|
+
read: (p) => this._readOne(meta, p.uri),
|
|
233
|
+
ls: (p) => this._ls(meta, p),
|
|
234
|
+
count: (p) => this._count(meta, p),
|
|
235
|
+
find: (p) => this._find(meta, p),
|
|
236
|
+
pushDownPattern: true,
|
|
237
|
+
pushDownCursor: true,
|
|
238
|
+
pushDownSortBy: true,
|
|
239
|
+
pushDownFind: true,
|
|
240
|
+
pushDownCount: true,
|
|
111
241
|
});
|
|
112
242
|
}
|
|
113
|
-
async _readOne(uri) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
243
|
+
async _readOne(meta, uri) {
|
|
244
|
+
try {
|
|
245
|
+
const { protocol, hostname, docId } = uriParts(uri);
|
|
246
|
+
const index = this._dataIndex(meta, protocol, hostname);
|
|
247
|
+
const doc = await this.executor.get(index, docId);
|
|
248
|
+
if (!doc)
|
|
249
|
+
return undefined;
|
|
250
|
+
if (meta.isBytes) {
|
|
251
|
+
return { payload: decodeBase64(doc.payload) };
|
|
252
|
+
}
|
|
253
|
+
return decodeRecord(meta.fields, doc);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
117
256
|
return undefined;
|
|
118
|
-
|
|
257
|
+
}
|
|
119
258
|
}
|
|
120
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Lucene regex query for shallow direct-leaves under `docPrefix`.
|
|
261
|
+
* Pattern push-down: splice the glob's regex body in place of the
|
|
262
|
+
* default `[^/]+`. Cursor push-down: wrap the regex in a `bool`
|
|
263
|
+
* with a `range` on `path.keyword` (`gt` for asc, `lt` for desc).
|
|
264
|
+
*/
|
|
265
|
+
_leafQuery(docPrefix, pattern, cursorPath, sortOrder) {
|
|
266
|
+
const body = pattern !== undefined ? saveGlobToRegexBody(pattern) : "[^/]+";
|
|
267
|
+
const regex = {
|
|
268
|
+
regexp: {
|
|
269
|
+
"path.keyword": `${escapeLuceneRegex(docPrefix)}${body}`,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
if (cursorPath === undefined)
|
|
273
|
+
return regex;
|
|
274
|
+
const rangeKey = sortOrder === "desc" ? "lt" : "gt";
|
|
121
275
|
return {
|
|
276
|
+
bool: {
|
|
277
|
+
must: [
|
|
278
|
+
regex,
|
|
279
|
+
{ range: { "path.keyword": { [rangeKey]: cursorPath } } },
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async _ls(meta, parsed) {
|
|
285
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
286
|
+
const { params } = parsed;
|
|
287
|
+
const format = params.format ?? "full";
|
|
288
|
+
const { protocol, hostname, docId } = uriParts(parsed.uri);
|
|
289
|
+
const index = this._dataIndex(meta, protocol, hostname);
|
|
290
|
+
// Cursor URI uses the same protocol+hostname as the prefix (it's
|
|
291
|
+
// from a previous page of the same query), so just strip those
|
|
292
|
+
// off to get the path-keyword cursor value.
|
|
293
|
+
const cursorPath = params.cursor !== undefined
|
|
294
|
+
? uriParts(params.cursor).docId
|
|
295
|
+
: undefined;
|
|
296
|
+
const body = {
|
|
297
|
+
query: this._leafQuery(docId, params.pattern, cursorPath, params.sortOrder),
|
|
298
|
+
};
|
|
299
|
+
// sortBy: "uri" sorts on `path.keyword` (uri is encoded as
|
|
300
|
+
// protocol+hostname+path; path.keyword is the comparable suffix
|
|
301
|
+
// we already index on). Any other value must be a declared field;
|
|
302
|
+
// dispatch only routes non-uri sortBy through here when
|
|
303
|
+
// pushDownSortBy is true, which it is for this backend.
|
|
304
|
+
//
|
|
305
|
+
// ES needs the right field name for sort: text fields are
|
|
306
|
+
// unsortable directly (fielddata is off by default in ES 8+) and
|
|
307
|
+
// must target the `.keyword` subfield instead. Numeric / date /
|
|
308
|
+
// boolean fields sort on the field itself. `json` fields aren't
|
|
309
|
+
// sortable, so we omit the sort clause and let dispatch's
|
|
310
|
+
// post-sort fallback handle them.
|
|
311
|
+
const dir = params.sortOrder === "desc" ? "desc" : "asc";
|
|
312
|
+
if (params.sortBy === "uri") {
|
|
313
|
+
body.sort = [{ "path.keyword": dir }];
|
|
314
|
+
}
|
|
315
|
+
else if (params.sortBy !== undefined && meta.declared.has(params.sortBy)) {
|
|
316
|
+
const esField = esSortField(meta, params.sortBy);
|
|
317
|
+
if (esField !== null)
|
|
318
|
+
body.sort = [{ [esField]: dir }];
|
|
319
|
+
}
|
|
320
|
+
if (params.limit !== undefined) {
|
|
321
|
+
const page = params.page ?? 1;
|
|
322
|
+
body.size = params.limit;
|
|
323
|
+
body.from = (page - 1) * params.limit;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
body.size = 10_000;
|
|
327
|
+
}
|
|
328
|
+
if (format === "uris")
|
|
329
|
+
body._source = false;
|
|
330
|
+
let result;
|
|
331
|
+
try {
|
|
332
|
+
result = await this.executor.search(index, body);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return format === "uris" ? [] : [];
|
|
336
|
+
}
|
|
337
|
+
if (format === "uris") {
|
|
338
|
+
return result.hits.map((hit) => `${protocol}://${hostname}/${hit._id}`);
|
|
339
|
+
}
|
|
340
|
+
return result.hits.map((hit) => {
|
|
341
|
+
const uri = `${protocol}://${hostname}/${hit._id}`;
|
|
342
|
+
if (!hit._source)
|
|
343
|
+
return [uri, undefined];
|
|
344
|
+
if (meta.isBytes) {
|
|
345
|
+
return [uri, { payload: decodeBase64(hit._source.payload) }];
|
|
346
|
+
}
|
|
347
|
+
return [uri, decodeRecord(meta.fields, hit._source)];
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Lucene regex query for the deep walk (`fn=find`) under
|
|
352
|
+
* `docPrefix`. Mirrors `_leafQuery` but REQUIRES a pattern: parseUrl
|
|
353
|
+
* guarantees `params.pattern` is populated whenever fn=find
|
|
354
|
+
* dispatches here (URI-embedded glob or dual-accept `?pattern=`).
|
|
355
|
+
* If pattern is absent we throw rather than silently widening to
|
|
356
|
+
* `.*` — that would conflate "find everything" with a parser bug.
|
|
357
|
+
*
|
|
358
|
+
* The composed body uses `saveGlobToRegexBody` (which emits `.*` for
|
|
359
|
+
* `**`, crossing `/`), prepended with the escaped `docPrefix`. The
|
|
360
|
+
* resulting regex is prefix-anchored on the keyword field — Lucene
|
|
361
|
+
* restricts the term-dictionary scan to terms starting with the
|
|
362
|
+
* required literal prefix, so even broad `**` queries don't degrade
|
|
363
|
+
* to a full term scan.
|
|
364
|
+
*/
|
|
365
|
+
_subtreeQuery(docPrefix, pattern, cursorPath, sortOrder) {
|
|
366
|
+
if (pattern === undefined) {
|
|
367
|
+
throw new Error(`${STORE_NAME}: fn=find handler reached without params.pattern set ` +
|
|
368
|
+
`(parser invariant violated)`);
|
|
369
|
+
}
|
|
370
|
+
const body = saveGlobToRegexBody(pattern);
|
|
371
|
+
const regex = {
|
|
122
372
|
regexp: {
|
|
123
|
-
"path.keyword": `${escapeLuceneRegex(docPrefix)}
|
|
373
|
+
"path.keyword": `${escapeLuceneRegex(docPrefix)}${body}`,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
if (cursorPath === undefined)
|
|
377
|
+
return regex;
|
|
378
|
+
const rangeKey = sortOrder === "desc" ? "lt" : "gt";
|
|
379
|
+
return {
|
|
380
|
+
bool: {
|
|
381
|
+
must: [
|
|
382
|
+
regex,
|
|
383
|
+
{ range: { "path.keyword": { [rangeKey]: cursorPath } } },
|
|
384
|
+
],
|
|
124
385
|
},
|
|
125
386
|
};
|
|
126
387
|
}
|
|
127
|
-
async
|
|
388
|
+
async _find(meta, parsed) {
|
|
128
389
|
validateReadParams(parsed.params, STORE_NAME);
|
|
129
390
|
const { params } = parsed;
|
|
130
391
|
const format = params.format ?? "full";
|
|
131
|
-
const {
|
|
392
|
+
const { protocol, hostname, docId } = uriParts(parsed.uri);
|
|
393
|
+
const index = this._dataIndex(meta, protocol, hostname);
|
|
394
|
+
const cursorPath = params.cursor !== undefined
|
|
395
|
+
? uriParts(params.cursor).docId
|
|
396
|
+
: undefined;
|
|
132
397
|
const body = {
|
|
133
|
-
query: this.
|
|
398
|
+
query: this._subtreeQuery(docId, params.pattern, cursorPath, params.sortOrder),
|
|
134
399
|
};
|
|
400
|
+
const dir = params.sortOrder === "desc" ? "desc" : "asc";
|
|
135
401
|
if (params.sortBy === "uri") {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
402
|
+
body.sort = [{ "path.keyword": dir }];
|
|
403
|
+
}
|
|
404
|
+
else if (params.sortBy !== undefined && meta.declared.has(params.sortBy)) {
|
|
405
|
+
const esField = esSortField(meta, params.sortBy);
|
|
406
|
+
if (esField !== null)
|
|
407
|
+
body.sort = [{ [esField]: dir }];
|
|
140
408
|
}
|
|
141
409
|
if (params.limit !== undefined) {
|
|
142
410
|
const page = params.page ?? 1;
|
|
@@ -148,27 +416,56 @@ export class ElasticsearchStore {
|
|
|
148
416
|
}
|
|
149
417
|
if (format === "uris")
|
|
150
418
|
body._source = false;
|
|
151
|
-
|
|
419
|
+
let result;
|
|
420
|
+
try {
|
|
421
|
+
result = await this.executor.search(index, body);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return format === "uris" ? [] : [];
|
|
425
|
+
}
|
|
152
426
|
if (format === "uris") {
|
|
153
|
-
return result.hits.map((hit) =>
|
|
427
|
+
return result.hits.map((hit) => `${protocol}://${hostname}/${hit._id}`);
|
|
154
428
|
}
|
|
155
|
-
return result.hits.map((hit) =>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
429
|
+
return result.hits.map((hit) => {
|
|
430
|
+
const uri = `${protocol}://${hostname}/${hit._id}`;
|
|
431
|
+
if (!hit._source)
|
|
432
|
+
return [uri, undefined];
|
|
433
|
+
if (meta.isBytes) {
|
|
434
|
+
return [uri, { payload: decodeBase64(hit._source.payload) }];
|
|
435
|
+
}
|
|
436
|
+
return [uri, decodeRecord(meta.fields, hit._source)];
|
|
437
|
+
});
|
|
159
438
|
}
|
|
160
|
-
async _count(parsed) {
|
|
161
|
-
|
|
162
|
-
|
|
439
|
+
async _count(meta, parsed) {
|
|
440
|
+
const { protocol, hostname, docId } = uriParts(parsed.uri);
|
|
441
|
+
const index = this._dataIndex(meta, protocol, hostname);
|
|
442
|
+
const cursorPath = parsed.params.cursor !== undefined
|
|
443
|
+
? uriParts(parsed.params.cursor).docId
|
|
444
|
+
: undefined;
|
|
445
|
+
try {
|
|
446
|
+
return await this.executor.count(index, {
|
|
447
|
+
query: this._leafQuery(docId, parsed.params.pattern, cursorPath, parsed.params.sortOrder),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return 0;
|
|
163
452
|
}
|
|
164
|
-
const { index, docId } = uriToIndexAndDocId(parsed.uri, this.indexPrefix);
|
|
165
|
-
return await this.executor.count(index, { query: this._leafQuery(docId) });
|
|
166
453
|
}
|
|
167
|
-
|
|
454
|
+
// ── Delete ───────────────────────────────────────────────────────
|
|
455
|
+
async delete(meta, uris) {
|
|
456
|
+
if (uris.length === 0)
|
|
457
|
+
return [];
|
|
458
|
+
if ((await this._readMetaSignature(meta.support.entity)) !== meta.signature) {
|
|
459
|
+
return uris.map((uri) => ({
|
|
460
|
+
success: false,
|
|
461
|
+
...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
168
464
|
const results = [];
|
|
169
465
|
for (const uri of uris) {
|
|
170
466
|
try {
|
|
171
|
-
const {
|
|
467
|
+
const { protocol, hostname, docId } = uriParts(uri);
|
|
468
|
+
const index = this._dataIndex(meta, protocol, hostname);
|
|
172
469
|
await this.executor.delete(index, docId);
|
|
173
470
|
results.push({ success: true });
|
|
174
471
|
}
|
|
@@ -181,7 +478,7 @@ export class ElasticsearchStore {
|
|
|
181
478
|
}
|
|
182
479
|
return results;
|
|
183
480
|
}
|
|
184
|
-
// ── Status
|
|
481
|
+
// ── Status / capabilities ────────────────────────────────────────
|
|
185
482
|
async status() {
|
|
186
483
|
try {
|
|
187
484
|
const ok = await this.executor.ping();
|
|
@@ -189,13 +486,15 @@ export class ElasticsearchStore {
|
|
|
189
486
|
return {
|
|
190
487
|
status: "unhealthy",
|
|
191
488
|
message: "Elasticsearch cluster is not reachable",
|
|
192
|
-
fns: ["read", "ls", "count"],
|
|
489
|
+
fns: ["read", "ls", "count", "find"],
|
|
193
490
|
};
|
|
194
491
|
}
|
|
492
|
+
const schema = await this._listProvisionedEntities();
|
|
195
493
|
return {
|
|
196
494
|
status: "healthy",
|
|
197
495
|
message: "Elasticsearch store is operational",
|
|
198
|
-
|
|
496
|
+
schema: schema.map((s) => `entity:${s}`),
|
|
497
|
+
fns: ["read", "ls", "count", "find"],
|
|
199
498
|
details: { indexPrefix: this.indexPrefix },
|
|
200
499
|
};
|
|
201
500
|
}
|
|
@@ -203,11 +502,71 @@ export class ElasticsearchStore {
|
|
|
203
502
|
return {
|
|
204
503
|
status: "unhealthy",
|
|
205
504
|
message: error instanceof Error ? error.message : String(error),
|
|
206
|
-
fns: ["read", "ls", "count"],
|
|
505
|
+
fns: ["read", "ls", "count", "find"],
|
|
207
506
|
};
|
|
208
507
|
}
|
|
209
508
|
}
|
|
210
509
|
capabilities() {
|
|
211
510
|
return { atomicBatch: false };
|
|
212
511
|
}
|
|
512
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
513
|
+
/** Per-entity, per-`protocol_hostname` data index. */
|
|
514
|
+
_dataIndex(meta, protocol, hostname) {
|
|
515
|
+
return `${this.indexPrefix}_${meta.indexInfix}${protocol}_${hostname}`;
|
|
516
|
+
}
|
|
517
|
+
/** Single meta index holding `{name → signature}` provisioning records. */
|
|
518
|
+
_metaIndex() {
|
|
519
|
+
return `${this.indexPrefix}_${META_INDEX_SUFFIX}`;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Read the persisted signature for an entity, or `null` if no
|
|
523
|
+
* provisioning record exists.
|
|
524
|
+
*/
|
|
525
|
+
async _readMetaSignature(entityName) {
|
|
526
|
+
try {
|
|
527
|
+
const doc = await this.executor.get(this._metaIndex(), entityName);
|
|
528
|
+
if (!doc)
|
|
529
|
+
return null;
|
|
530
|
+
return doc.signature ?? null;
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Scan the meta index for every provisioned entity name. Used by
|
|
538
|
+
* `status()` to advertise the entities a peer can talk to.
|
|
539
|
+
*/
|
|
540
|
+
async _listProvisionedEntities() {
|
|
541
|
+
try {
|
|
542
|
+
const result = await this.executor.search(this._metaIndex(), {
|
|
543
|
+
query: { match_all: {} },
|
|
544
|
+
size: 10_000,
|
|
545
|
+
});
|
|
546
|
+
return result.hits.map((h) => h._id);
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Collect any `ReadableStream` values on `bytes`-tagged fields into
|
|
554
|
+
* `Uint8Array` and return a shallow-copied record.
|
|
555
|
+
*/
|
|
556
|
+
async _normaliseBytesFields(record, bytesFields) {
|
|
557
|
+
const out = { ...record };
|
|
558
|
+
for (const name of bytesFields) {
|
|
559
|
+
const v = out[name];
|
|
560
|
+
if (v === undefined || v === null)
|
|
561
|
+
continue;
|
|
562
|
+
if (v instanceof Uint8Array)
|
|
563
|
+
continue;
|
|
564
|
+
if (v instanceof ReadableStream) {
|
|
565
|
+
out[name] = await toBytes(v);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
throw new Error(`field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
|
|
569
|
+
}
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
213
572
|
}
|
package/esm/entity-store.d.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* `fn=ls` and `fn=count` follow the same conventions as `Store`
|
|
44
44
|
* (URI lists and numbers — see `./read.ts`).
|
|
45
45
|
*/
|
|
46
|
-
import type { DeleteResult, Output, StatusResult } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
46
|
+
import type { DeleteResult, Output, StatusResult } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
47
47
|
import type { EntityMeta, EntityRecord, EntitySchema } from "./entity.js";
|
|
48
48
|
import type { StoreCapabilities, StoreWriteResult } from "./types.js";
|
|
49
49
|
export interface EntityStore<TMeta extends EntityMeta = EntityMeta> {
|
package/esm/errors.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* one line and the error code consistent (`STORAGE_ERROR` for
|
|
9
9
|
* anything that came from the underlying driver).
|
|
10
10
|
*/
|
|
11
|
-
import type { B3ndError } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
11
|
+
import type { B3ndError } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
12
12
|
/**
|
|
13
13
|
* Build the structured-failure half of a Store result from a thrown
|
|
14
14
|
* value. `fallback` is used when `err` isn't an `Error` (rare). `uri`
|