@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
package/esm/mongo/store.js
CHANGED
|
@@ -1,26 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MongoStore — MongoDB implementation of
|
|
2
|
+
* MongoStore — MongoDB implementation of `EntityStore`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
4
|
+
* One layout for every schema: a per-entity collection named
|
|
5
|
+
* `{prefix}_{entity}_data` with a `uri` primary key and one document
|
|
6
|
+
* key per supported field, plus `createdAt` / `updatedAt`.
|
|
7
|
+
* `BYTES_ENTITY` is just an entity with one `payload` field of BSON
|
|
8
|
+
* binary — no legacy special case.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* ## Lifecycle
|
|
11
|
+
*
|
|
12
|
+
* `entitySupport(schema)` is pure — it runs `planFields` and returns
|
|
13
|
+
* a `MongoEntityMeta` with the target collection name, declared field
|
|
14
|
+
* set, bytes-field set, canonical-tag-per-field signature, and the
|
|
15
|
+
* support report. No IO.
|
|
16
|
+
*
|
|
17
|
+
* `provisionEntity(meta)` materialises the entity:
|
|
18
|
+
*
|
|
19
|
+
* 1. Reads the meta collection `{prefix}_meta` for a prior entry
|
|
20
|
+
* under `meta.support.entity`.
|
|
21
|
+
* 2. If found with a matching signature → no-op (idempotent).
|
|
22
|
+
* 3. If found with a different signature → throws (collision: a
|
|
23
|
+
* same-name entity is already live in a different shape).
|
|
24
|
+
* 4. Otherwise: creates the entity collection, creates the `uri`
|
|
25
|
+
* index, and upserts the meta entry.
|
|
26
|
+
*
|
|
27
|
+
* `entityStatus(meta)` reads the meta collection and reports `"live"`
|
|
28
|
+
* only when the stored signature matches. Mongo is schemaless on the
|
|
29
|
+
* wire — without an explicit meta record there is nothing to
|
|
30
|
+
* introspect, so the meta collection is how shape collisions get
|
|
31
|
+
* detected (the same approach MemoryStore uses; the SQL backends get
|
|
32
|
+
* the same guarantee for free via column-type introspection).
|
|
33
|
+
*
|
|
34
|
+
* Writes/reads/deletes consume the meta directly: no per-call cache
|
|
35
|
+
* gate. The store enforces the extras check (a record with keys not
|
|
36
|
+
* declared in `meta` produces a per-entry write failure); value-type
|
|
37
|
+
* enforcement is left to higher layers because Mongo will accept any
|
|
38
|
+
* value at the BSON level.
|
|
39
|
+
*
|
|
40
|
+
* `fn=ls` / `fn=count` push down to MongoDB via a regex prefix filter
|
|
41
|
+
* on `uri` that enforces the shallow-direct-leaves contract
|
|
42
|
+
* (`^<prefix>[^/]+$`).
|
|
43
|
+
*
|
|
44
|
+
* `fn=find` (v2 §3.3) reuses the same regex query shape but composes
|
|
45
|
+
* the body for the broader grammar (`**` → `.*`, crosses `/`). The
|
|
46
|
+
* literal prefix stays escaped; the glob tail is compiled via
|
|
47
|
+
* `saveGlobToRegexBody` — the same helper `_ls` uses, which already
|
|
48
|
+
* emits `.*` for `**` regardless of fn (the wildcard semantics are a
|
|
49
|
+
* property of the glob token, not the verb). The `[^/]+` shallow
|
|
50
|
+
* fallback that `_ls` uses for "no pattern" is NOT used here: `find`
|
|
51
|
+
* always arrives with a pattern (parseUrl mirrors the URI-embedded
|
|
52
|
+
* glob into `params.pattern`; dispatch pushes it through under
|
|
53
|
+
* `pushDownFind: true`).
|
|
54
|
+
*
|
|
55
|
+
* Perf note: an anchored `^<literal-prefix><body>$` regex with a long
|
|
56
|
+
* literal prefix can use the per-collection `uri` index for prefix
|
|
57
|
+
* scans; broader patterns (`**` at the start) degrade to a collection
|
|
58
|
+
* scan. The contract advertises `find` with that caveat — callers that
|
|
59
|
+
* need fast deep walks under shallow prefixes should keep their root
|
|
60
|
+
* literal (`<root><room>/alice/**` scans only `alice/`).
|
|
61
|
+
*
|
|
62
|
+
* Mongo does not provide a portable single-collection batch
|
|
63
|
+
* write/delete primitive that fits the package's per-entry result
|
|
64
|
+
* contract, so writes happen one document at a time and
|
|
65
|
+
* `capabilities.atomicBatch` is `false`. Stream-shaped values on
|
|
66
|
+
* bytes fields are collected to `Uint8Array` up front per entry.
|
|
11
67
|
*/
|
|
12
|
-
import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
|
|
13
68
|
import { dispatchRead } from "../dispatch.js";
|
|
14
69
|
import { storageFailure } from "../errors.js";
|
|
15
70
|
import { toBytes } from "../payload.js";
|
|
16
71
|
import { validateReadParams } from "../read.js";
|
|
72
|
+
import { saveGlobToRegexBody } from "../glob.js";
|
|
73
|
+
import { TYPE_TAGS, } from "../entity.js";
|
|
74
|
+
import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
|
|
75
|
+
import { computeSignature, planFields } from "./fields.js";
|
|
17
76
|
const STORE_NAME = "MongoStore";
|
|
18
|
-
/** Escape special regex characters for safe use in a
|
|
77
|
+
/** Escape special regex characters for safe use in a `$regex` filter. */
|
|
19
78
|
function escapeRegex(input) {
|
|
20
79
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
80
|
}
|
|
22
81
|
/**
|
|
23
|
-
*
|
|
82
|
+
* Normalise a Mongo document field into a `Uint8Array`. The driver may
|
|
24
83
|
* surface BSON `Binary` (which exposes `.buffer`) or a raw
|
|
25
84
|
* `Uint8Array` depending on options.
|
|
26
85
|
*/
|
|
@@ -37,123 +96,136 @@ function docToBytes(value) {
|
|
|
37
96
|
value.buffer instanceof ArrayBuffer) {
|
|
38
97
|
return new Uint8Array(value.buffer);
|
|
39
98
|
}
|
|
40
|
-
throw new Error(
|
|
99
|
+
throw new Error(`${STORE_NAME}: unexpected payload type ${typeof value}`);
|
|
100
|
+
}
|
|
101
|
+
function entityCollectionName(prefix, entityName) {
|
|
102
|
+
if (!isValidIdentifier(entityName)) {
|
|
103
|
+
throw new Error(`${STORE_NAME}: entity name '${entityName}' must match ${IDENTIFIER_PATTERN.source}`);
|
|
104
|
+
}
|
|
105
|
+
return `${prefix}_${entityName}_data`;
|
|
106
|
+
}
|
|
107
|
+
function metaCollectionName(prefix) {
|
|
108
|
+
return `${prefix}_meta`;
|
|
41
109
|
}
|
|
42
110
|
export class MongoStore {
|
|
43
|
-
|
|
111
|
+
collectionPrefix;
|
|
44
112
|
executor;
|
|
45
|
-
constructor(
|
|
46
|
-
if (!
|
|
47
|
-
throw new Error("
|
|
113
|
+
constructor(collectionPrefix, executor) {
|
|
114
|
+
if (!collectionPrefix)
|
|
115
|
+
throw new Error("collectionPrefix is required");
|
|
116
|
+
if (!isValidIdentifier(collectionPrefix)) {
|
|
117
|
+
throw new Error(`collectionPrefix must match ${IDENTIFIER_PATTERN.source}; got '${collectionPrefix}'`);
|
|
118
|
+
}
|
|
48
119
|
if (!executor)
|
|
49
120
|
throw new Error("executor is required");
|
|
50
|
-
this.
|
|
121
|
+
this.collectionPrefix = collectionPrefix;
|
|
51
122
|
this.executor = executor;
|
|
52
123
|
}
|
|
53
|
-
// ──
|
|
124
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
54
125
|
entitySupport(schema) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
126
|
+
const { fields, unsupported } = planFields(schema.fields);
|
|
127
|
+
const support = {
|
|
128
|
+
entity: schema.name,
|
|
129
|
+
supported: fields.map((f) => f.name),
|
|
130
|
+
unsupported,
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
support,
|
|
134
|
+
collectionName: entityCollectionName(this.collectionPrefix, schema.name),
|
|
135
|
+
fields,
|
|
136
|
+
declared: new Set(fields.map((f) => f.name)),
|
|
137
|
+
bytesFields: new Set(fields.filter((f) => f.tag === TYPE_TAGS.BYTES).map((f) => f.name)),
|
|
138
|
+
signature: computeSignature(schema.name, fields),
|
|
139
|
+
};
|
|
62
140
|
}
|
|
63
|
-
|
|
64
|
-
|
|
141
|
+
async entityStatus(meta) {
|
|
142
|
+
const metaDoc = await this.executor
|
|
143
|
+
.collection(metaCollectionName(this.collectionPrefix))
|
|
144
|
+
.findOne({ _id: meta.support.entity });
|
|
145
|
+
if (!metaDoc)
|
|
146
|
+
return "unprovisioned";
|
|
147
|
+
return metaDoc.signature === meta.signature ? "live" : "unprovisioned";
|
|
65
148
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
149
|
+
async provisionEntity(meta) {
|
|
150
|
+
const metaColl = this.executor.collection(metaCollectionName(this.collectionPrefix));
|
|
151
|
+
const existing = await metaColl.findOne({ _id: meta.support.entity });
|
|
152
|
+
if (existing) {
|
|
153
|
+
if (existing.signature !== meta.signature) {
|
|
154
|
+
throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
|
|
155
|
+
`provisioned with a different shape at collection '${meta.collectionName}'`);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await this.executor.createCollection(meta.collectionName);
|
|
160
|
+
await this.executor.collection(meta.collectionName).createIndex({ uri: 1 });
|
|
161
|
+
await metaColl.updateOne({ _id: meta.support.entity }, { $set: { _id: meta.support.entity, signature: meta.signature } }, { upsert: true });
|
|
71
162
|
}
|
|
72
|
-
// ──
|
|
73
|
-
async
|
|
74
|
-
|
|
75
|
-
|
|
163
|
+
// ── Write ────────────────────────────────────────────────────────
|
|
164
|
+
async write(meta, entries) {
|
|
165
|
+
if (entries.length === 0)
|
|
166
|
+
return [];
|
|
167
|
+
const out = new Array(entries.length);
|
|
168
|
+
const coll = this.executor.collection(meta.collectionName);
|
|
169
|
+
for (let i = 0; i < entries.length; i++) {
|
|
170
|
+
const { uri, record } = entries[i];
|
|
171
|
+
const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
|
|
172
|
+
if (extras.length > 0) {
|
|
173
|
+
out[i] = {
|
|
174
|
+
success: false,
|
|
175
|
+
...storageFailure(new Error(`${STORE_NAME}: record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
|
|
176
|
+
};
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
76
179
|
try {
|
|
77
|
-
const
|
|
78
|
-
await
|
|
79
|
-
$set: {
|
|
80
|
-
|
|
81
|
-
payload: bytes,
|
|
82
|
-
updatedAt: new Date(),
|
|
83
|
-
},
|
|
180
|
+
const doc = await buildDocument(meta, uri, record);
|
|
181
|
+
await coll.updateOne({ uri }, {
|
|
182
|
+
$set: { ...doc, updatedAt: new Date() },
|
|
183
|
+
$setOnInsert: { createdAt: new Date() },
|
|
84
184
|
}, { upsert: true });
|
|
85
|
-
|
|
185
|
+
out[i] = { success: true };
|
|
86
186
|
}
|
|
87
187
|
catch (err) {
|
|
88
|
-
|
|
188
|
+
out[i] = {
|
|
89
189
|
success: false,
|
|
90
|
-
...storageFailure(err, "Write failed",
|
|
91
|
-
}
|
|
190
|
+
...storageFailure(err, "Write failed", uri),
|
|
191
|
+
};
|
|
92
192
|
}
|
|
93
193
|
}
|
|
94
|
-
return
|
|
194
|
+
return out;
|
|
95
195
|
}
|
|
96
|
-
|
|
196
|
+
// ── Read ─────────────────────────────────────────────────────────
|
|
197
|
+
read(meta, urls) {
|
|
97
198
|
return dispatchRead(urls, STORE_NAME, {
|
|
98
|
-
read: (p) => this._readOne(p.uri),
|
|
99
|
-
ls: (p) => this._ls(p),
|
|
100
|
-
count: (p) => this._count(p),
|
|
199
|
+
read: (p) => this._readOne(meta, p.uri),
|
|
200
|
+
ls: (p) => this._ls(meta, p),
|
|
201
|
+
count: (p) => this._count(meta, p),
|
|
202
|
+
find: (p) => this._find(meta, p),
|
|
203
|
+
pushDownPattern: true,
|
|
204
|
+
pushDownCursor: true,
|
|
205
|
+
pushDownSortBy: true,
|
|
206
|
+
pushDownFind: true,
|
|
101
207
|
});
|
|
102
208
|
}
|
|
103
|
-
async
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
/** Build the shallow-direct-leaves regex filter for a prefix. */
|
|
110
|
-
_leafFilter(prefixUri) {
|
|
111
|
-
return { uri: { $regex: `^${escapeRegex(prefixUri)}[^/]+$` } };
|
|
112
|
-
}
|
|
113
|
-
async _ls(parsed) {
|
|
114
|
-
validateReadParams(parsed.params, STORE_NAME);
|
|
115
|
-
const { params } = parsed;
|
|
116
|
-
const format = params.format ?? "full";
|
|
117
|
-
const options = {};
|
|
118
|
-
if (params.sortBy === "uri") {
|
|
119
|
-
options.sort = { uri: params.sortOrder === "desc" ? -1 : 1 };
|
|
120
|
-
}
|
|
121
|
-
if (params.limit !== undefined) {
|
|
122
|
-
const page = params.page ?? 1;
|
|
123
|
-
options.limit = params.limit;
|
|
124
|
-
options.skip = (page - 1) * params.limit;
|
|
125
|
-
}
|
|
126
|
-
if (format === "uris") {
|
|
127
|
-
options.projection = { uri: 1, _id: 0 };
|
|
128
|
-
}
|
|
129
|
-
const docs = await this.executor.findMany(this._leafFilter(parsed.uri), options);
|
|
130
|
-
if (format === "uris")
|
|
131
|
-
return docs.map((d) => d.uri);
|
|
132
|
-
return docs.map((d) => [d.uri, docToBytes(d.payload)]);
|
|
133
|
-
}
|
|
134
|
-
async _count(parsed) {
|
|
135
|
-
if (parsed.params.pattern !== undefined) {
|
|
136
|
-
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
137
|
-
}
|
|
138
|
-
return await this.executor.countDocuments(this._leafFilter(parsed.uri));
|
|
139
|
-
}
|
|
140
|
-
async _deleteBytes(uris) {
|
|
141
|
-
const results = [];
|
|
142
|
-
for (const uri of uris) {
|
|
209
|
+
async delete(meta, uris) {
|
|
210
|
+
if (uris.length === 0)
|
|
211
|
+
return [];
|
|
212
|
+
const coll = this.executor.collection(meta.collectionName);
|
|
213
|
+
const out = new Array(uris.length);
|
|
214
|
+
for (let i = 0; i < uris.length; i++) {
|
|
143
215
|
try {
|
|
144
|
-
await
|
|
145
|
-
|
|
216
|
+
await coll.deleteOne({ uri: uris[i] });
|
|
217
|
+
out[i] = { success: true };
|
|
146
218
|
}
|
|
147
219
|
catch (err) {
|
|
148
|
-
|
|
220
|
+
out[i] = {
|
|
149
221
|
success: false,
|
|
150
|
-
...storageFailure(err, "Delete failed",
|
|
151
|
-
}
|
|
222
|
+
...storageFailure(err, "Delete failed", uris[i]),
|
|
223
|
+
};
|
|
152
224
|
}
|
|
153
225
|
}
|
|
154
|
-
return
|
|
226
|
+
return out;
|
|
155
227
|
}
|
|
156
|
-
// ── Status
|
|
228
|
+
// ── Status / capabilities ────────────────────────────────────────
|
|
157
229
|
async status() {
|
|
158
230
|
try {
|
|
159
231
|
const ok = await this.executor.ping();
|
|
@@ -161,25 +233,206 @@ export class MongoStore {
|
|
|
161
233
|
return {
|
|
162
234
|
status: "unhealthy",
|
|
163
235
|
message: "MongoDB ping failed",
|
|
164
|
-
fns: ["read", "ls", "count"],
|
|
236
|
+
fns: ["read", "ls", "find", "count"],
|
|
165
237
|
};
|
|
166
238
|
}
|
|
167
239
|
return {
|
|
168
240
|
status: "healthy",
|
|
169
241
|
message: "MongoDB store is operational",
|
|
170
|
-
fns: ["read", "ls", "count"],
|
|
171
|
-
details: {
|
|
242
|
+
fns: ["read", "ls", "find", "count"],
|
|
243
|
+
details: { collectionPrefix: this.collectionPrefix },
|
|
172
244
|
};
|
|
173
245
|
}
|
|
174
246
|
catch (error) {
|
|
175
247
|
return {
|
|
176
248
|
status: "unhealthy",
|
|
177
249
|
message: error instanceof Error ? error.message : String(error),
|
|
178
|
-
fns: ["read", "ls", "count"],
|
|
250
|
+
fns: ["read", "ls", "find", "count"],
|
|
179
251
|
};
|
|
180
252
|
}
|
|
181
253
|
}
|
|
182
254
|
capabilities() {
|
|
183
255
|
return { atomicBatch: false };
|
|
184
256
|
}
|
|
257
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
258
|
+
async _readOne(meta, uri) {
|
|
259
|
+
const doc = await this.executor.collection(meta.collectionName).findOne({
|
|
260
|
+
uri,
|
|
261
|
+
});
|
|
262
|
+
if (!doc)
|
|
263
|
+
return undefined;
|
|
264
|
+
if (meta.fields.length === 0)
|
|
265
|
+
return {};
|
|
266
|
+
return adaptDocForRead(meta, doc);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Compose the Mongo filter for shallow-direct-leaves under
|
|
270
|
+
* `prefixUri`, optionally tightened by a glob `pattern` (regex body
|
|
271
|
+
* spliced in place of `[^/]+`) and a `cursor` (`$gt` or `$lt` on
|
|
272
|
+
* `uri` matching the active sort order).
|
|
273
|
+
*/
|
|
274
|
+
_leafFilter(prefixUri, pattern, cursor, sortOrder) {
|
|
275
|
+
const body = pattern !== undefined ? saveGlobToRegexBody(pattern) : "[^/]+";
|
|
276
|
+
const uriFilter = {
|
|
277
|
+
$regex: `^${escapeRegex(prefixUri)}${body}$`,
|
|
278
|
+
};
|
|
279
|
+
if (cursor !== undefined) {
|
|
280
|
+
uriFilter[sortOrder === "desc" ? "$lt" : "$gt"] = cursor;
|
|
281
|
+
}
|
|
282
|
+
return { uri: uriFilter };
|
|
283
|
+
}
|
|
284
|
+
async _ls(meta, parsed) {
|
|
285
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
286
|
+
const { params } = parsed;
|
|
287
|
+
const format = params.format ?? "full";
|
|
288
|
+
const options = {};
|
|
289
|
+
// sortBy: "uri" sorts on the implicit URI key; any other value
|
|
290
|
+
// must be a declared field. Dispatch only routes non-uri sortBy
|
|
291
|
+
// through here when pushDownSortBy is true, which it is for this
|
|
292
|
+
// backend; we still gate on meta.declared so an unrecognised
|
|
293
|
+
// field falls through to no sort rather than a Mongo error.
|
|
294
|
+
if (params.sortBy === "uri") {
|
|
295
|
+
options.sort = { uri: params.sortOrder === "desc" ? -1 : 1 };
|
|
296
|
+
}
|
|
297
|
+
else if (params.sortBy !== undefined && meta.declared.has(params.sortBy)) {
|
|
298
|
+
options.sort = {
|
|
299
|
+
[params.sortBy]: params.sortOrder === "desc" ? -1 : 1,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (params.limit !== undefined) {
|
|
303
|
+
const page = params.page ?? 1;
|
|
304
|
+
options.limit = params.limit;
|
|
305
|
+
options.skip = (page - 1) * params.limit;
|
|
306
|
+
}
|
|
307
|
+
if (format === "uris") {
|
|
308
|
+
options.projection = { uri: 1, _id: 0 };
|
|
309
|
+
}
|
|
310
|
+
const docs = await this.executor.collection(meta.collectionName).findMany(this._leafFilter(parsed.uri, params.pattern, params.cursor, params.sortOrder), options);
|
|
311
|
+
if (format === "uris")
|
|
312
|
+
return docs.map((d) => d.uri);
|
|
313
|
+
return docs.map((d) => [
|
|
314
|
+
d.uri,
|
|
315
|
+
adaptDocForRead(meta, d),
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Compose the Mongo filter for a `find` (deep walk) under
|
|
320
|
+
* `prefixUri`. Same shape as `_leafFilter` — `$regex` anchored on
|
|
321
|
+
* both ends, optional cursor `$gt`/`$lt` — but the body is the glob
|
|
322
|
+
* compiled with `**` → `.*` semantics (crosses `/`). For find we
|
|
323
|
+
* REQUIRE a pattern: parseUrl guarantees `params.pattern` is set
|
|
324
|
+
* whenever fn=find dispatches to this handler (URI-embedded glob or
|
|
325
|
+
* the dual-accept `?pattern=` form). If pattern is absent we throw
|
|
326
|
+
* loudly rather than silently widening to `.*` — that would conflate
|
|
327
|
+
* "find everything" with a parser bug.
|
|
328
|
+
*/
|
|
329
|
+
_subtreeFilter(prefixUri, pattern, cursor, sortOrder) {
|
|
330
|
+
if (pattern === undefined) {
|
|
331
|
+
throw new Error(`${STORE_NAME}: fn=find handler reached without params.pattern set ` +
|
|
332
|
+
`(parser invariant violated)`);
|
|
333
|
+
}
|
|
334
|
+
const body = saveGlobToRegexBody(pattern);
|
|
335
|
+
const uriFilter = {
|
|
336
|
+
$regex: `^${escapeRegex(prefixUri)}${body}$`,
|
|
337
|
+
};
|
|
338
|
+
if (cursor !== undefined) {
|
|
339
|
+
uriFilter[sortOrder === "desc" ? "$lt" : "$gt"] = cursor;
|
|
340
|
+
}
|
|
341
|
+
return { uri: uriFilter };
|
|
342
|
+
}
|
|
343
|
+
async _find(meta, parsed) {
|
|
344
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
345
|
+
const { params } = parsed;
|
|
346
|
+
const format = params.format ?? "full";
|
|
347
|
+
const options = {};
|
|
348
|
+
if (params.sortBy === "uri") {
|
|
349
|
+
options.sort = { uri: params.sortOrder === "desc" ? -1 : 1 };
|
|
350
|
+
}
|
|
351
|
+
else if (params.sortBy !== undefined && meta.declared.has(params.sortBy)) {
|
|
352
|
+
options.sort = {
|
|
353
|
+
[params.sortBy]: params.sortOrder === "desc" ? -1 : 1,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (params.limit !== undefined) {
|
|
357
|
+
const page = params.page ?? 1;
|
|
358
|
+
options.limit = params.limit;
|
|
359
|
+
options.skip = (page - 1) * params.limit;
|
|
360
|
+
}
|
|
361
|
+
if (format === "uris") {
|
|
362
|
+
options.projection = { uri: 1, _id: 0 };
|
|
363
|
+
}
|
|
364
|
+
const docs = await this.executor.collection(meta.collectionName).findMany(this._subtreeFilter(parsed.uri, params.pattern, params.cursor, params.sortOrder), options);
|
|
365
|
+
if (format === "uris")
|
|
366
|
+
return docs.map((d) => d.uri);
|
|
367
|
+
return docs.map((d) => [
|
|
368
|
+
d.uri,
|
|
369
|
+
adaptDocForRead(meta, d),
|
|
370
|
+
]);
|
|
371
|
+
}
|
|
372
|
+
async _count(meta, parsed) {
|
|
373
|
+
return await this.executor.collection(meta.collectionName).countDocuments(this._leafFilter(parsed.uri, parsed.params.pattern, parsed.params.cursor, parsed.params.sortOrder));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Build the document to upsert: `uri` + every declared field, with
|
|
378
|
+
* stream-shaped bytes fields collected up front so a stream failure
|
|
379
|
+
* surfaces as a per-entry write failure (caught by the caller) before
|
|
380
|
+
* any I/O against the collection.
|
|
381
|
+
*/
|
|
382
|
+
async function buildDocument(meta, uri, record) {
|
|
383
|
+
const doc = { uri };
|
|
384
|
+
for (const field of meta.fields) {
|
|
385
|
+
const value = record[field.name];
|
|
386
|
+
if (value === undefined)
|
|
387
|
+
continue;
|
|
388
|
+
if (meta.bytesFields.has(field.name)) {
|
|
389
|
+
if (value === null) {
|
|
390
|
+
doc[field.name] = null;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (value instanceof Uint8Array) {
|
|
394
|
+
doc[field.name] = value;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (value instanceof ReadableStream) {
|
|
398
|
+
doc[field.name] = await toBytes(value);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`${STORE_NAME}: field '${field.name}' must be Uint8Array or ReadableStream, got ${typeof value}`);
|
|
402
|
+
}
|
|
403
|
+
doc[field.name] = adaptValueForWrite(field, value);
|
|
404
|
+
}
|
|
405
|
+
return doc;
|
|
406
|
+
}
|
|
407
|
+
function adaptValueForWrite(field, value) {
|
|
408
|
+
if (value === null)
|
|
409
|
+
return null;
|
|
410
|
+
if (field.tag === TYPE_TAGS.TIMESTAMP) {
|
|
411
|
+
if (value instanceof Date)
|
|
412
|
+
return value.toISOString();
|
|
413
|
+
if (typeof value === "number")
|
|
414
|
+
return new Date(value).toISOString();
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
function adaptDocForRead(meta, doc) {
|
|
420
|
+
const rec = {};
|
|
421
|
+
for (const field of meta.fields) {
|
|
422
|
+
const v = doc[field.name];
|
|
423
|
+
if (v === undefined || v === null) {
|
|
424
|
+
rec[field.name] = undefined;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (field.tag === TYPE_TAGS.BYTES) {
|
|
428
|
+
rec[field.name] = docToBytes(v);
|
|
429
|
+
}
|
|
430
|
+
else if (field.tag === TYPE_TAGS.TIMESTAMP) {
|
|
431
|
+
rec[field.name] = v instanceof Date ? v : new Date(v);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
rec[field.name] = v;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return rec;
|
|
185
438
|
}
|
package/esm/postgres/store.d.ts
CHANGED
|
@@ -27,12 +27,33 @@
|
|
|
27
27
|
* predicate (`uri LIKE prefix% AND uri NOT LIKE prefix%/%`) is
|
|
28
28
|
* enforced in the WHERE clause against the entity's table.
|
|
29
29
|
*
|
|
30
|
+
* `fn=find` (v2 §3.5) push down to SQL too — same shape minus the
|
|
31
|
+
* `NOT LIKE %/%` shallow clause. With `pushDownFind: true` dispatch
|
|
32
|
+
* trusts the backend to combine the URI-embedded glob with pagination,
|
|
33
|
+
* cursor, and sort inside the query.
|
|
34
|
+
*
|
|
35
|
+
* `fn=count` honours deep (`**`) URLs by dropping the shallow clause
|
|
36
|
+
* when `parsed.glob.includes("**")`. With `pushDownCount: true`,
|
|
37
|
+
* dispatch routes every count shape — shallow and deep — to the
|
|
38
|
+
* backend's own `COUNT(*)` (faster than walking-and-counting through
|
|
39
|
+
* find).
|
|
40
|
+
*
|
|
41
|
+
* ## LIKE-injection safety
|
|
42
|
+
*
|
|
43
|
+
* Every value spliced into a SQL `LIKE` pattern via concatenation is
|
|
44
|
+
* pre-escaped with `escapeSqlLike()` (`%` → `\%`, `_` → `\_`,
|
|
45
|
+
* `\` → `\\`) and the surrounding clauses declare `ESCAPE '\\'`. This
|
|
46
|
+
* prevents a URI prefix or cursor containing `%` or `_` from matching
|
|
47
|
+
* siblings (e.g. `store://r/100%/` must NOT match `store://r/100x/...`
|
|
48
|
+
* under `fn=ls`). The glob body produced by `globToSqlLike` already
|
|
49
|
+
* encodes its own escaping for literal `%`/`_` in the pattern source.
|
|
50
|
+
*
|
|
30
51
|
* Writes are wrapped in a transaction (`capabilities.atomicBatch =
|
|
31
52
|
* true`). Stream-shaped values on `BYTEA` columns are collected to
|
|
32
53
|
* `Uint8Array` *before* the transaction opens, so a stream failure
|
|
33
54
|
* can't leave a half-applied commit.
|
|
34
55
|
*/
|
|
35
|
-
import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
56
|
+
import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
36
57
|
import type { EntityStore } from "../entity-store.js";
|
|
37
58
|
import type { EntityMeta, EntityRecord, EntitySchema } from "../entity.js";
|
|
38
59
|
import type { StoreCapabilities, StoreWriteResult } from "../types.js";
|
|
@@ -70,6 +91,34 @@ export declare class PostgresStore implements EntityStore<PostgresEntityMeta> {
|
|
|
70
91
|
capabilities(): StoreCapabilities;
|
|
71
92
|
private _readOne;
|
|
72
93
|
private _ls;
|
|
94
|
+
/**
|
|
95
|
+
* `fn=find` handler — deep walk under the prefix. Same query shape as
|
|
96
|
+
* `_ls` minus the `NOT LIKE prefix || '%/%'` shallow clause: every
|
|
97
|
+
* descendant URI matches the prefix; the URI-embedded glob (passed in
|
|
98
|
+
* via `params.pattern` by the parser) tightens the set with a SQL
|
|
99
|
+
* `LIKE` body. `pushDownFind: true` is declared so dispatch trusts the
|
|
100
|
+
* backend with pattern + pagination + sort.
|
|
101
|
+
*/
|
|
102
|
+
private _find;
|
|
103
|
+
/**
|
|
104
|
+
* Shared SELECT for `_ls` and `_find`. The only structural difference
|
|
105
|
+
* is the shallow clause: `_ls` includes `uri NOT LIKE prefix || '%/%'
|
|
106
|
+
* ESCAPE '\\'`; `_find` drops it. Everything else — pattern body,
|
|
107
|
+
* cursor, sort, pagination — is identical.
|
|
108
|
+
*
|
|
109
|
+
* LIKE-injection safety: `parsed.uri` is pre-escaped with
|
|
110
|
+
* `escapeSqlLike()` before being bound. The bind value is the escaped
|
|
111
|
+
* prefix; the surrounding `LIKE $1 || '%' ESCAPE '\\'` then treats
|
|
112
|
+
* any `%`/`_` in the original prefix as literals.
|
|
113
|
+
*/
|
|
114
|
+
private _listQuery;
|
|
115
|
+
/**
|
|
116
|
+
* `fn=count` handler. Honours both shallow (`*`/`?` or no glob) and
|
|
117
|
+
* deep (`**`) URL shapes — the latter drops the `NOT LIKE %/%`
|
|
118
|
+
* predicate so descendants at any depth get counted. With
|
|
119
|
+
* `pushDownCount: true`, dispatch routes every count call through
|
|
120
|
+
* here (no walk-then-length fallback).
|
|
121
|
+
*/
|
|
73
122
|
private _count;
|
|
74
123
|
}
|
|
75
124
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/postgres/store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/postgres/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAO3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAI5C;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C;AAqDD,qBAAa,aAAc,YAAW,WAAW,CAAC,kBAAkB,CAAC;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;gBAE3B,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW;IAetD,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB;IAkBjD,YAAY,CAChB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IA4B9B,eAAe,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBxD,KAAK,CACT,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA4E9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAcjB,MAAM,CACV,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,YAAY,EAAE,CAAC;IAkBpB,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAoBrC,YAAY,IAAI,iBAAiB;YAMnB,QAAQ;YAqBR,GAAG;IAOjB;;;;;;;OAOG;YACW,KAAK;IAOnB;;;;;;;;;;OAUG;YACW,UAAU;IAiExB;;;;;;OAMG;YACW,MAAM;CAwBrB"}
|