@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/url.d.ts
CHANGED
|
@@ -9,24 +9,46 @@
|
|
|
9
9
|
* (memory, postgres, mongo, sqlite, fs, ipfs, s3, elasticsearch,
|
|
10
10
|
* localstorage, indexeddb) honor the grammar defined here.
|
|
11
11
|
*
|
|
12
|
-
* A **url** = a uri (the canonical resource identifier
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* A **url** = a uri (the canonical resource identifier, possibly carrying
|
|
13
|
+
* wildcards in its path) plus a query string of read parameters. The
|
|
14
|
+
* literal portion of the uri (everything before the first wildcard) is
|
|
15
|
+
* the routing identity used for storage keys and observe-match keys.
|
|
16
|
+
* The query string carries request-time-only directives that shape the
|
|
17
|
+
* response without changing identity.
|
|
17
18
|
*
|
|
18
|
-
* Grammar:
|
|
19
|
+
* Grammar (v2 — see `.cc-chat/20260625121936-grammar-shape/output.md`):
|
|
19
20
|
*
|
|
20
|
-
* <uri>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
|
|
21
|
+
* <uri-with-glob>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
|
|
22
|
+
*
|
|
23
|
+
* Glob tokens in `<uri-with-glob>`'s path:
|
|
24
|
+
* - `?` exactly one non-`/` character
|
|
25
|
+
* - `*` zero or more non-`/` characters (one segment)
|
|
26
|
+
* - `**` zero or more segments (any chars including `/`); **find-only**
|
|
21
27
|
*
|
|
22
28
|
* Reserved `fn` values:
|
|
23
|
-
* - `read` — point read (default for
|
|
24
|
-
* - `ls` — list
|
|
25
|
-
* - `
|
|
29
|
+
* - `read` — point read (default for bare uris)
|
|
30
|
+
* - `ls` — list direct children (URI has `?`/`*` but no `**`)
|
|
31
|
+
* - `find` — recursive walk (URI contains `**`)
|
|
32
|
+
* - `count` — count of entries
|
|
26
33
|
* - `x-*.*` — provider-defined extension functions
|
|
27
34
|
*
|
|
35
|
+
* Validation rules (§3.4 — silent-default-trap mitigation):
|
|
36
|
+
* - A URI with wildcards in its path MUST carry an explicit `?fn=`.
|
|
37
|
+
* - `?fn=ls` rejects URIs containing `**`.
|
|
38
|
+
* - `?fn=find` requires `**` (or a legacy `?pattern=` carrying `**`).
|
|
39
|
+
* - `?fn=count` accepts any URI shape.
|
|
40
|
+
* - A bare URI (no wildcards) keeps today's behavior — `?fn=` may be
|
|
41
|
+
* omitted; defaults to `read` (no trailing slash) or `ls` (trailing
|
|
42
|
+
* slash, equivalent to `<uri>*?fn=ls`).
|
|
43
|
+
*
|
|
44
|
+
* Dual-accept transition: `?pattern=<glob>` still parses (logged as
|
|
45
|
+
* deprecated once per process). Internally normalized to the same
|
|
46
|
+
* `{prefix, glob}` shape as the URI-embedded form. Removal scheduled
|
|
47
|
+
* one minor version out.
|
|
48
|
+
*
|
|
28
49
|
* Standard params (meaningful only for some fns; stores ignore the rest):
|
|
29
|
-
* `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format
|
|
50
|
+
* `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format`,
|
|
51
|
+
* `fields`.
|
|
30
52
|
*
|
|
31
53
|
* Anything matching `x-<ns>.<key>` lands in the `ext` map and is passed
|
|
32
54
|
* opaquely to the executing store. Callers inspect ext entries by
|
|
@@ -44,12 +66,22 @@ export interface ReadParams {
|
|
|
44
66
|
sortOrder?: string;
|
|
45
67
|
pattern?: string;
|
|
46
68
|
format?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Comma-separated list of field names to project from each returned
|
|
71
|
+
* record. Applies to `fn=read` and `fn=ls&format=full`. Fields not
|
|
72
|
+
* declared in the entity meta are silently absent from the
|
|
73
|
+
* projection — the store does not error on unknown projection
|
|
74
|
+
* fields, they simply do not appear. `format=uris` ignores `fields`
|
|
75
|
+
* since it returns no record payloads.
|
|
76
|
+
*/
|
|
77
|
+
fields?: string[];
|
|
47
78
|
}
|
|
48
79
|
/**
|
|
49
80
|
* A parsed url decomposed into routing identity (`uri`), function
|
|
50
81
|
* (`fn`), standard read parameters (`params`), and the `x-*` extension
|
|
51
82
|
* bag (`ext`), plus the structural fields shared with WHATWG URL
|
|
52
|
-
* (`protocol`, `hostname`, `path`, `program`)
|
|
83
|
+
* (`protocol`, `hostname`, `path`, `program`), plus the v2 glob split
|
|
84
|
+
* (`glob`).
|
|
53
85
|
*/
|
|
54
86
|
export interface ParsedUrl {
|
|
55
87
|
/** Scheme without trailing `://` (e.g. `"mutable"`, `"b3nd"`). */
|
|
@@ -60,15 +92,64 @@ export interface ParsedUrl {
|
|
|
60
92
|
path: string;
|
|
61
93
|
/** `protocol://hostname` — the routing root above the path. */
|
|
62
94
|
program: string;
|
|
63
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* Routing identity. When the source url contains no wildcards in its
|
|
97
|
+
* path, this is the full URI as written. When the source contains
|
|
98
|
+
* wildcards, this is the **literal prefix** — everything before the
|
|
99
|
+
* first wildcard segment, including the trailing `/`. Backends key
|
|
100
|
+
* storage / observe matching off this value.
|
|
101
|
+
*/
|
|
64
102
|
uri: string;
|
|
65
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Glob tail extracted from the source URI's path (v2 §3.3). Empty
|
|
105
|
+
* string when the source carried no wildcards in its path. When the
|
|
106
|
+
* caller used the dual-accept `?pattern=` form, this carries that
|
|
107
|
+
* value verbatim (back-compat normalization).
|
|
108
|
+
*/
|
|
109
|
+
glob: string;
|
|
110
|
+
/** Reserved fn (`read`/`ls`/`find`/`count`) or `x-<ns>.<name>` extension. */
|
|
66
111
|
fn: string;
|
|
67
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Standard read params. `params.pattern` is populated from either
|
|
114
|
+
* the URI-embedded glob (preferred) or the legacy `?pattern=` query
|
|
115
|
+
* param (deprecated) — both end up here so call sites that read
|
|
116
|
+
* `params.pattern` keep working through the transition.
|
|
117
|
+
*/
|
|
68
118
|
params: ReadParams;
|
|
69
119
|
/** Bag of `x-*` extension query params (every key starts with `x-`). */
|
|
70
120
|
ext: Record<string, string>;
|
|
71
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Split a uri's path into its literal prefix and its glob tail.
|
|
124
|
+
*
|
|
125
|
+
* Walks the path looking for the first segment that contains a glob
|
|
126
|
+
* metacharacter (`*` or `?`). Returns the literal prefix (everything
|
|
127
|
+
* up to and including the `/` before that segment) and the glob tail
|
|
128
|
+
* (the wildcard segment and everything after it). When no wildcards
|
|
129
|
+
* are present, returns the whole URI as `prefix` and `""` as `glob`.
|
|
130
|
+
*
|
|
131
|
+
* The walk is path-aware so `?` is treated as a wildcard inside path
|
|
132
|
+
* segments only (the function never sees a query string — callers
|
|
133
|
+
* strip `?<query>` first).
|
|
134
|
+
*
|
|
135
|
+
* Examples:
|
|
136
|
+
* "x://a/b/c" → { prefix: "x://a/b/c", glob: "" }
|
|
137
|
+
* "x://a/b/**" → { prefix: "x://a/b/", glob: "**" }
|
|
138
|
+
* "x://a/**\/x.md" → { prefix: "x://a/", glob: "**\/x.md" }
|
|
139
|
+
* "x://a/al*" → { prefix: "x://a/", glob: "al*" }
|
|
140
|
+
* "x://a/al?" → { prefix: "x://a/", glob: "al?" }
|
|
141
|
+
* "**" → { prefix: "", glob: "**" }
|
|
142
|
+
*
|
|
143
|
+
* Note: the input must already have `?<query>` stripped — the function
|
|
144
|
+
* has no way to distinguish a literal `?` in the path from a query
|
|
145
|
+
* separator. `parseUrl` handles the split before calling.
|
|
146
|
+
*/
|
|
147
|
+
export declare function splitLocatorGlob(uri: string): {
|
|
148
|
+
prefix: string;
|
|
149
|
+
glob: string;
|
|
150
|
+
};
|
|
151
|
+
/** Test-only hook to reset the deprecation gate between tests. */
|
|
152
|
+
export declare function _resetPatternDeprecationLogForTests(): void;
|
|
72
153
|
/**
|
|
73
154
|
* Parse a url into its structural fields, function, params, and extensions.
|
|
74
155
|
*
|
|
@@ -77,11 +158,23 @@ export interface ParsedUrl {
|
|
|
77
158
|
* (e.g. `b3nd://count/mutable://users/`) stays inside `path` — no
|
|
78
159
|
* special-casing.
|
|
79
160
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
161
|
+
* Glob extraction: the path is scanned for the first wildcard segment
|
|
162
|
+
* (`splitLocatorGlob`). When wildcards are present, `parsed.uri` is the
|
|
163
|
+
* literal prefix; `parsed.glob` is the wildcard tail; `parsed.params.pattern`
|
|
164
|
+
* is set to the glob so downstream code can read either field. When the
|
|
165
|
+
* caller used the legacy `?pattern=` form instead, the same fields are
|
|
166
|
+
* populated and a deprecation is logged once per process.
|
|
83
167
|
*
|
|
84
|
-
*
|
|
168
|
+
* Verb defaults & validation (v2 §3.4):
|
|
169
|
+
* - bare URI (no wildcards), no `?fn=` → defaults to `read` for non-
|
|
170
|
+
* trailing-slash, `ls` for trailing-slash (legacy convenience)
|
|
171
|
+
* - any URI with wildcards in the path → `?fn=` is REQUIRED
|
|
172
|
+
* - `**` in the URI is valid only under `?fn=find` (or `?fn=count`)
|
|
173
|
+
* - `*`/`?` (no `**`) is valid under `?fn=ls` or `?fn=find` or `?fn=count`
|
|
174
|
+
* - `?fn=count` accepts any URI shape
|
|
175
|
+
*
|
|
176
|
+
* Numeric params (`limit`, `page`) are coerced; throws on NaN. Unknown
|
|
177
|
+
* standard params throw.
|
|
85
178
|
*/
|
|
86
179
|
export declare function parseUrl(url: string): ParsedUrl;
|
|
87
180
|
/**
|
|
@@ -91,6 +184,12 @@ export declare function parseUrl(url: string): ParsedUrl;
|
|
|
91
184
|
*
|
|
92
185
|
* Omits `fn=` when it matches the trailing-slash default. Throws if any
|
|
93
186
|
* `ext` key does not start with `x-`.
|
|
187
|
+
*
|
|
188
|
+
* Note: `buildUrl` writes the URI verbatim — it does not splice a glob
|
|
189
|
+
* back into the path. Callers that have a `ParsedUrl` produced by
|
|
190
|
+
* `parseUrl` from a URI-embedded-glob source should reconstruct the
|
|
191
|
+
* full url manually if they need to round-trip the original string; the
|
|
192
|
+
* cheap path is `<originalUrl>` which they already have.
|
|
94
193
|
*/
|
|
95
194
|
export declare function buildUrl(parsed: {
|
|
96
195
|
uri: string;
|
|
@@ -103,6 +202,10 @@ export declare function buildUrl(parsed: {
|
|
|
103
202
|
* request-time-only and never participates in routing or storage
|
|
104
203
|
* keying. Cheap helper for stores and callers that need the bare uri
|
|
105
204
|
* without paying for full parse.
|
|
205
|
+
*
|
|
206
|
+
* Note: this returns the URI as written, **including** any embedded
|
|
207
|
+
* glob (`<uri-with-glob>`). For just the literal routing prefix, parse
|
|
208
|
+
* the url and read `parsed.uri`.
|
|
106
209
|
*/
|
|
107
210
|
export declare function uriOf(url: string): string;
|
|
108
211
|
//# sourceMappingURL=url.d.ts.map
|
package/esm/url.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../src/url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX;;;;;OAKG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,GACV;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAuClC;AAcD,kEAAkE;AAClE,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAkJ/C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B,GAAG,MAAM,CAwBT;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGzC"}
|
package/esm/url.js
CHANGED
|
@@ -9,29 +9,129 @@
|
|
|
9
9
|
* (memory, postgres, mongo, sqlite, fs, ipfs, s3, elasticsearch,
|
|
10
10
|
* localstorage, indexeddb) honor the grammar defined here.
|
|
11
11
|
*
|
|
12
|
-
* A **url** = a uri (the canonical resource identifier
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* A **url** = a uri (the canonical resource identifier, possibly carrying
|
|
13
|
+
* wildcards in its path) plus a query string of read parameters. The
|
|
14
|
+
* literal portion of the uri (everything before the first wildcard) is
|
|
15
|
+
* the routing identity used for storage keys and observe-match keys.
|
|
16
|
+
* The query string carries request-time-only directives that shape the
|
|
17
|
+
* response without changing identity.
|
|
17
18
|
*
|
|
18
|
-
* Grammar:
|
|
19
|
+
* Grammar (v2 — see `.cc-chat/20260625121936-grammar-shape/output.md`):
|
|
19
20
|
*
|
|
20
|
-
* <uri>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
|
|
21
|
+
* <uri-with-glob>[?fn=<fn>][&<param>=<value>...][&x-<ns>.<key>=<value>...]
|
|
22
|
+
*
|
|
23
|
+
* Glob tokens in `<uri-with-glob>`'s path:
|
|
24
|
+
* - `?` exactly one non-`/` character
|
|
25
|
+
* - `*` zero or more non-`/` characters (one segment)
|
|
26
|
+
* - `**` zero or more segments (any chars including `/`); **find-only**
|
|
21
27
|
*
|
|
22
28
|
* Reserved `fn` values:
|
|
23
|
-
* - `read` — point read (default for
|
|
24
|
-
* - `ls` — list
|
|
25
|
-
* - `
|
|
29
|
+
* - `read` — point read (default for bare uris)
|
|
30
|
+
* - `ls` — list direct children (URI has `?`/`*` but no `**`)
|
|
31
|
+
* - `find` — recursive walk (URI contains `**`)
|
|
32
|
+
* - `count` — count of entries
|
|
26
33
|
* - `x-*.*` — provider-defined extension functions
|
|
27
34
|
*
|
|
35
|
+
* Validation rules (§3.4 — silent-default-trap mitigation):
|
|
36
|
+
* - A URI with wildcards in its path MUST carry an explicit `?fn=`.
|
|
37
|
+
* - `?fn=ls` rejects URIs containing `**`.
|
|
38
|
+
* - `?fn=find` requires `**` (or a legacy `?pattern=` carrying `**`).
|
|
39
|
+
* - `?fn=count` accepts any URI shape.
|
|
40
|
+
* - A bare URI (no wildcards) keeps today's behavior — `?fn=` may be
|
|
41
|
+
* omitted; defaults to `read` (no trailing slash) or `ls` (trailing
|
|
42
|
+
* slash, equivalent to `<uri>*?fn=ls`).
|
|
43
|
+
*
|
|
44
|
+
* Dual-accept transition: `?pattern=<glob>` still parses (logged as
|
|
45
|
+
* deprecated once per process). Internally normalized to the same
|
|
46
|
+
* `{prefix, glob}` shape as the URI-embedded form. Removal scheduled
|
|
47
|
+
* one minor version out.
|
|
48
|
+
*
|
|
28
49
|
* Standard params (meaningful only for some fns; stores ignore the rest):
|
|
29
|
-
* `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format
|
|
50
|
+
* `limit`, `page`, `cursor`, `sortBy`, `sortOrder`, `pattern`, `format`,
|
|
51
|
+
* `fields`.
|
|
30
52
|
*
|
|
31
53
|
* Anything matching `x-<ns>.<key>` lands in the `ext` map and is passed
|
|
32
54
|
* opaquely to the executing store. Callers inspect ext entries by
|
|
33
55
|
* their flat string key (e.g. `ext["x-feed.cursor"]`).
|
|
34
56
|
*/
|
|
57
|
+
/**
|
|
58
|
+
* Split a uri's path into its literal prefix and its glob tail.
|
|
59
|
+
*
|
|
60
|
+
* Walks the path looking for the first segment that contains a glob
|
|
61
|
+
* metacharacter (`*` or `?`). Returns the literal prefix (everything
|
|
62
|
+
* up to and including the `/` before that segment) and the glob tail
|
|
63
|
+
* (the wildcard segment and everything after it). When no wildcards
|
|
64
|
+
* are present, returns the whole URI as `prefix` and `""` as `glob`.
|
|
65
|
+
*
|
|
66
|
+
* The walk is path-aware so `?` is treated as a wildcard inside path
|
|
67
|
+
* segments only (the function never sees a query string — callers
|
|
68
|
+
* strip `?<query>` first).
|
|
69
|
+
*
|
|
70
|
+
* Examples:
|
|
71
|
+
* "x://a/b/c" → { prefix: "x://a/b/c", glob: "" }
|
|
72
|
+
* "x://a/b/**" → { prefix: "x://a/b/", glob: "**" }
|
|
73
|
+
* "x://a/**\/x.md" → { prefix: "x://a/", glob: "**\/x.md" }
|
|
74
|
+
* "x://a/al*" → { prefix: "x://a/", glob: "al*" }
|
|
75
|
+
* "x://a/al?" → { prefix: "x://a/", glob: "al?" }
|
|
76
|
+
* "**" → { prefix: "", glob: "**" }
|
|
77
|
+
*
|
|
78
|
+
* Note: the input must already have `?<query>` stripped — the function
|
|
79
|
+
* has no way to distinguish a literal `?` in the path from a query
|
|
80
|
+
* separator. `parseUrl` handles the split before calling.
|
|
81
|
+
*/
|
|
82
|
+
export function splitLocatorGlob(uri) {
|
|
83
|
+
// Find the scheme boundary; wildcards before the path are nonsense
|
|
84
|
+
// but we don't reject them here — splitLocatorGlob is a pure
|
|
85
|
+
// structural split. The caller (parseUrl) sees the result and the
|
|
86
|
+
// grammar validator decides what's legal.
|
|
87
|
+
const schemeIdx = uri.indexOf("://");
|
|
88
|
+
let pathStart;
|
|
89
|
+
if (schemeIdx < 0) {
|
|
90
|
+
pathStart = 0;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const rest = uri.slice(schemeIdx + 3);
|
|
94
|
+
const slash = rest.indexOf("/");
|
|
95
|
+
pathStart = slash < 0 ? uri.length : schemeIdx + 3 + slash;
|
|
96
|
+
}
|
|
97
|
+
// Walk segment by segment from pathStart. A segment containing `*`
|
|
98
|
+
// or `?` is the first wildcard segment; the split point is the `/`
|
|
99
|
+
// that begins that segment.
|
|
100
|
+
let segStart = pathStart;
|
|
101
|
+
const len = uri.length;
|
|
102
|
+
for (let i = pathStart; i <= len; i++) {
|
|
103
|
+
if (i === len || uri.charCodeAt(i) === 47 /* / */) {
|
|
104
|
+
const seg = uri.slice(segStart, i);
|
|
105
|
+
if (seg.includes("*") || seg.includes("?")) {
|
|
106
|
+
// Prefix is everything up to (and including) the `/` before
|
|
107
|
+
// this segment. segStart points at the first char of the
|
|
108
|
+
// segment, so prefix = uri.slice(0, segStart). When the
|
|
109
|
+
// wildcard segment is at the root (no preceding `/`),
|
|
110
|
+
// segStart === pathStart and prefix is just the program.
|
|
111
|
+
return {
|
|
112
|
+
prefix: uri.slice(0, segStart),
|
|
113
|
+
glob: uri.slice(segStart),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
segStart = i + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { prefix: uri, glob: "" };
|
|
120
|
+
}
|
|
121
|
+
// ── Deprecation log gate ────────────────────────────────────────────
|
|
122
|
+
let _patternDeprecationLogged = false;
|
|
123
|
+
function logPatternDeprecationOnce() {
|
|
124
|
+
if (_patternDeprecationLogged)
|
|
125
|
+
return;
|
|
126
|
+
_patternDeprecationLogged = true;
|
|
127
|
+
console.warn("b3nd-save: `?pattern=` query param is deprecated; use URI-embedded glob " +
|
|
128
|
+
'(e.g. "<root>/**?fn=find"). Support will be removed one minor version out.');
|
|
129
|
+
}
|
|
130
|
+
/** Test-only hook to reset the deprecation gate between tests. */
|
|
131
|
+
export function _resetPatternDeprecationLogForTests() {
|
|
132
|
+
_patternDeprecationLogged = false;
|
|
133
|
+
}
|
|
134
|
+
// ── Parser ───────────────────────────────────────────────────────────
|
|
35
135
|
/**
|
|
36
136
|
* Parse a url into its structural fields, function, params, and extensions.
|
|
37
137
|
*
|
|
@@ -40,23 +140,35 @@
|
|
|
40
140
|
* (e.g. `b3nd://count/mutable://users/`) stays inside `path` — no
|
|
41
141
|
* special-casing.
|
|
42
142
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
143
|
+
* Glob extraction: the path is scanned for the first wildcard segment
|
|
144
|
+
* (`splitLocatorGlob`). When wildcards are present, `parsed.uri` is the
|
|
145
|
+
* literal prefix; `parsed.glob` is the wildcard tail; `parsed.params.pattern`
|
|
146
|
+
* is set to the glob so downstream code can read either field. When the
|
|
147
|
+
* caller used the legacy `?pattern=` form instead, the same fields are
|
|
148
|
+
* populated and a deprecation is logged once per process.
|
|
46
149
|
*
|
|
47
|
-
*
|
|
150
|
+
* Verb defaults & validation (v2 §3.4):
|
|
151
|
+
* - bare URI (no wildcards), no `?fn=` → defaults to `read` for non-
|
|
152
|
+
* trailing-slash, `ls` for trailing-slash (legacy convenience)
|
|
153
|
+
* - any URI with wildcards in the path → `?fn=` is REQUIRED
|
|
154
|
+
* - `**` in the URI is valid only under `?fn=find` (or `?fn=count`)
|
|
155
|
+
* - `*`/`?` (no `**`) is valid under `?fn=ls` or `?fn=find` or `?fn=count`
|
|
156
|
+
* - `?fn=count` accepts any URI shape
|
|
157
|
+
*
|
|
158
|
+
* Numeric params (`limit`, `page`) are coerced; throws on NaN. Unknown
|
|
159
|
+
* standard params throw.
|
|
48
160
|
*/
|
|
49
161
|
export function parseUrl(url) {
|
|
50
162
|
const qIdx = url.indexOf("?");
|
|
51
|
-
const
|
|
163
|
+
const uriRaw = qIdx < 0 ? url : url.slice(0, qIdx);
|
|
52
164
|
const query = qIdx < 0 ? "" : url.slice(qIdx + 1);
|
|
53
|
-
const schemeIdx =
|
|
165
|
+
const schemeIdx = uriRaw.indexOf("://");
|
|
54
166
|
let protocol = "";
|
|
55
167
|
let hostname = "";
|
|
56
168
|
let path = "";
|
|
57
169
|
if (schemeIdx >= 0) {
|
|
58
|
-
protocol =
|
|
59
|
-
const rest =
|
|
170
|
+
protocol = uriRaw.slice(0, schemeIdx);
|
|
171
|
+
const rest = uriRaw.slice(schemeIdx + 3);
|
|
60
172
|
const slash = rest.indexOf("/");
|
|
61
173
|
if (slash < 0) {
|
|
62
174
|
hostname = rest;
|
|
@@ -68,13 +180,13 @@ export function parseUrl(url) {
|
|
|
68
180
|
}
|
|
69
181
|
}
|
|
70
182
|
else {
|
|
71
|
-
path =
|
|
183
|
+
path = uriRaw;
|
|
72
184
|
}
|
|
73
185
|
const program = protocol ? `${protocol}://${hostname}` : "";
|
|
186
|
+
// Split off any glob tail embedded in the path.
|
|
187
|
+
const { prefix: uriPrefix, glob: uriGlob } = splitLocatorGlob(uriRaw);
|
|
74
188
|
const sp = new URLSearchParams(query);
|
|
75
189
|
const explicitFn = sp.get("fn") ?? undefined;
|
|
76
|
-
const defaultFn = uri.endsWith("/") ? "ls" : "read";
|
|
77
|
-
const fn = explicitFn ?? defaultFn;
|
|
78
190
|
const params = {};
|
|
79
191
|
const ext = {};
|
|
80
192
|
for (const [key, value] of sp.entries()) {
|
|
@@ -97,15 +209,87 @@ export function parseUrl(url) {
|
|
|
97
209
|
case "cursor":
|
|
98
210
|
case "sortBy":
|
|
99
211
|
case "sortOrder":
|
|
100
|
-
case "pattern":
|
|
101
212
|
case "format":
|
|
102
213
|
params[key] = value;
|
|
103
214
|
break;
|
|
215
|
+
case "pattern":
|
|
216
|
+
params.pattern = value;
|
|
217
|
+
break;
|
|
218
|
+
case "fields":
|
|
219
|
+
params.fields = value.split(",").map((f) => f.trim()).filter(Boolean);
|
|
220
|
+
break;
|
|
104
221
|
default:
|
|
105
222
|
throw new Error(`Unknown read param: ${key}`);
|
|
106
223
|
}
|
|
107
224
|
}
|
|
108
|
-
|
|
225
|
+
// Reconcile URI-embedded glob with legacy `?pattern=`.
|
|
226
|
+
// Both populated → conflict → throw (caller has two intents in one URL).
|
|
227
|
+
// Only `?pattern=` populated → dual-accept transition; log deprecation;
|
|
228
|
+
// leave `parsed.uri` as the literal uriRaw (it has no wildcards) and
|
|
229
|
+
// `parsed.glob` carries the pattern value (the routing identity is
|
|
230
|
+
// still the literal uri).
|
|
231
|
+
// Only URI glob populated → preferred path; mirror into params.pattern
|
|
232
|
+
// so legacy push-down sites that read `params.pattern` keep working.
|
|
233
|
+
let effectiveGlob = uriGlob;
|
|
234
|
+
let effectiveUri = uriPrefix;
|
|
235
|
+
if (uriGlob && params.pattern !== undefined) {
|
|
236
|
+
throw new Error(`b3nd-save: cannot combine URI-embedded glob ("${uriGlob}") with ` +
|
|
237
|
+
`?pattern= query param ("${params.pattern}") — pick one`);
|
|
238
|
+
}
|
|
239
|
+
if (!uriGlob && params.pattern !== undefined) {
|
|
240
|
+
logPatternDeprecationOnce();
|
|
241
|
+
effectiveGlob = params.pattern;
|
|
242
|
+
effectiveUri = uriRaw;
|
|
243
|
+
}
|
|
244
|
+
if (uriGlob && params.pattern === undefined) {
|
|
245
|
+
params.pattern = uriGlob;
|
|
246
|
+
}
|
|
247
|
+
// Verb resolution. The default lookup uses the literal-uri shape
|
|
248
|
+
// (trailing slash → ls else read) but is only honoured when there
|
|
249
|
+
// are no wildcards anywhere.
|
|
250
|
+
const hasWildcards = effectiveGlob.length > 0;
|
|
251
|
+
let fn;
|
|
252
|
+
if (explicitFn !== undefined) {
|
|
253
|
+
fn = explicitFn;
|
|
254
|
+
}
|
|
255
|
+
else if (hasWildcards) {
|
|
256
|
+
throw new Error(`b3nd-save: URL contains wildcards but ?fn= is absent — declare ` +
|
|
257
|
+
`the verb explicitly (e.g. ?fn=find for "**", ?fn=ls for "*"/"?")`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
fn = uriRaw.endsWith("/") ? "ls" : "read";
|
|
261
|
+
}
|
|
262
|
+
// Verb/shape validation when wildcards are present (§3.4).
|
|
263
|
+
if (hasWildcards) {
|
|
264
|
+
const hasDoubleStar = effectiveGlob.includes("**");
|
|
265
|
+
if (fn === "ls" && hasDoubleStar) {
|
|
266
|
+
throw new Error(`b3nd-save: ?fn=ls rejects URLs containing "**" — use ?fn=find for ` +
|
|
267
|
+
`recursive walks`);
|
|
268
|
+
}
|
|
269
|
+
if (fn === "find" && !hasDoubleStar) {
|
|
270
|
+
throw new Error(`b3nd-save: ?fn=find requires "**" in the URL (or in legacy ` +
|
|
271
|
+
`?pattern=); got "${effectiveGlob}"`);
|
|
272
|
+
}
|
|
273
|
+
// ?fn=count accepts both shapes per §3.2; x-* extensions are
|
|
274
|
+
// provider-defined and pass through; ?fn=read with wildcards is
|
|
275
|
+
// nonsense and we reject it explicitly so callers don't get a
|
|
276
|
+
// confusing point-read attempt on a literal "**" key.
|
|
277
|
+
if (fn === "read") {
|
|
278
|
+
throw new Error(`b3nd-save: ?fn=read rejects URLs containing wildcards — use ?fn=ls ` +
|
|
279
|
+
`or ?fn=find`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
protocol,
|
|
284
|
+
hostname,
|
|
285
|
+
path,
|
|
286
|
+
program,
|
|
287
|
+
uri: effectiveUri,
|
|
288
|
+
glob: effectiveGlob,
|
|
289
|
+
fn,
|
|
290
|
+
params,
|
|
291
|
+
ext,
|
|
292
|
+
};
|
|
109
293
|
}
|
|
110
294
|
/**
|
|
111
295
|
* Serialize a parsed url back to its string form. Authoritative input is
|
|
@@ -114,6 +298,12 @@ export function parseUrl(url) {
|
|
|
114
298
|
*
|
|
115
299
|
* Omits `fn=` when it matches the trailing-slash default. Throws if any
|
|
116
300
|
* `ext` key does not start with `x-`.
|
|
301
|
+
*
|
|
302
|
+
* Note: `buildUrl` writes the URI verbatim — it does not splice a glob
|
|
303
|
+
* back into the path. Callers that have a `ParsedUrl` produced by
|
|
304
|
+
* `parseUrl` from a URI-embedded-glob source should reconstruct the
|
|
305
|
+
* full url manually if they need to round-trip the original string; the
|
|
306
|
+
* cheap path is `<originalUrl>` which they already have.
|
|
117
307
|
*/
|
|
118
308
|
export function buildUrl(parsed) {
|
|
119
309
|
const { uri, fn, params, ext } = parsed;
|
|
@@ -144,6 +334,10 @@ export function buildUrl(parsed) {
|
|
|
144
334
|
* request-time-only and never participates in routing or storage
|
|
145
335
|
* keying. Cheap helper for stores and callers that need the bare uri
|
|
146
336
|
* without paying for full parse.
|
|
337
|
+
*
|
|
338
|
+
* Note: this returns the URI as written, **including** any embedded
|
|
339
|
+
* glob (`<uri-with-glob>`). For just the literal routing prefix, parse
|
|
340
|
+
* the url and read `parsed.uri`.
|
|
147
341
|
*/
|
|
148
342
|
export function uriOf(url) {
|
|
149
343
|
const qIdx = url.indexOf("?");
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* `walkViaLs` — caller-invoked polyfill that simulates `fn=find`
|
|
4
|
+
* against stores that advertise only `fn=ls`.
|
|
5
|
+
*
|
|
6
|
+
* **When to use.** A caller wants every entry under a prefix (the
|
|
7
|
+
* recursive-listing use case `fn=find` solves) but is targeting a store
|
|
8
|
+
* whose `status().fns` includes `"ls"` and NOT `"find"`. Without this
|
|
9
|
+
* helper the caller would have to hand-roll the recursive walk.
|
|
10
|
+
*
|
|
11
|
+
* **When NOT to use.** If the target store advertises `"find"`, call
|
|
12
|
+
* `?fn=find` directly — it will be cheaper (push-down where possible,
|
|
13
|
+
* single network round-trip in many cases). `walkViaLs` is the explicit
|
|
14
|
+
* fallback, *never* an auto-engaged dispatch fallback (per v1 spec §7
|
|
15
|
+
* `.cc-chat/20260625093437-listing-spec/output.md`). Capability check
|
|
16
|
+
* is the caller's responsibility:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const status = await rig.status();
|
|
20
|
+
* const fns = status?.fns ?? [];
|
|
21
|
+
* const uris = fns.includes("find")
|
|
22
|
+
* ? await rig.read([`${prefix}**?fn=find&format=uris`])
|
|
23
|
+
* : await walkViaLs(rig.read.bind(rig), prefix, { format: "uris" });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* **Contract.** `walkViaLs` uses ONLY the standard b3nd PIN `read`
|
|
27
|
+
* surface — no privileged store access, no rig internals. It issues
|
|
28
|
+
* repeated shallow `?fn=ls&format=uris` calls level-by-level, recursing
|
|
29
|
+
* into each child whose own `?fn=ls&format=uris` returns at least one
|
|
30
|
+
* entry (probe-based directory detection — see Design notes below).
|
|
31
|
+
*
|
|
32
|
+
* **What this DOES NOT do:**
|
|
33
|
+
* - No streaming. The full result accumulates before returning.
|
|
34
|
+
* - No pagination. The helper always returns the complete walk; if
|
|
35
|
+
* the underlying store's `ls` paginates, callers must wrap that
|
|
36
|
+
* concern themselves (or push for `fn=find` support).
|
|
37
|
+
* - No parallelism. Each level is walked serially. This is a
|
|
38
|
+
* polyfill; the right answer for performance is `fn=find`.
|
|
39
|
+
* - No content-equivalence guarantees with `fn=find`. Push-down
|
|
40
|
+
* `fn=find` may sort/paginate differently. `walkViaLs` returns a
|
|
41
|
+
* stable depth-first traversal in the order each level's `ls`
|
|
42
|
+
* yields its children.
|
|
43
|
+
*
|
|
44
|
+
* **Cost model.** O(D) sequential round-trips where D is the count of
|
|
45
|
+
* non-empty directories under `prefix`. For deep trees this is much
|
|
46
|
+
* worse than a single `fn=find` push-down. Use only when capability
|
|
47
|
+
* forces it.
|
|
48
|
+
*
|
|
49
|
+
* **Design notes** (decisions documented for reviewers):
|
|
50
|
+
*
|
|
51
|
+
* 1. *Directory detection: probe via ls, not URI shape.* A naive
|
|
52
|
+
* "no trailing slash → leaf" or "no dot-extension → directory"
|
|
53
|
+
* heuristic would mis-classify common shapes (an entity named
|
|
54
|
+
* `notes` with no extension; a file URI ending in `/`). Probing
|
|
55
|
+
* via `?fn=ls&format=uris` is authoritative: if it returns any
|
|
56
|
+
* URIs, recurse; if it returns the empty set, treat as terminal.
|
|
57
|
+
* The cost is one extra ls per leaf URI — accepted because
|
|
58
|
+
* `walkViaLs` is already O(D) and the spec calls it out as a slow
|
|
59
|
+
* polyfill.
|
|
60
|
+
*
|
|
61
|
+
* 2. *Pattern filtering happens at the helper, not the wire.* The
|
|
62
|
+
* underlying store's `ls` only supports `*` / `?` (no `**` — that's
|
|
63
|
+
* why we're walking via ls). `compileSaveGlob` from `./glob.ts`
|
|
64
|
+
* applies the full v2 §3.3 grammar (including `**`) to the
|
|
65
|
+
* accumulated tail-URIs before returning. Callers see the same
|
|
66
|
+
* glob semantics they would from `fn=find`.
|
|
67
|
+
*
|
|
68
|
+
* 3. *No cursor pagination.* `fn=find` ships a trailing cursor slot
|
|
69
|
+
* (v2 §3.5); this polyfill always returns the full accumulated set
|
|
70
|
+
* and never appends a cursor slot. Callers needing pagination must
|
|
71
|
+
* upgrade to `fn=find`-capable storage.
|
|
72
|
+
*/
|
|
73
|
+
import type { Output } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
74
|
+
/**
|
|
75
|
+
* Options for {@link walkViaLs}. Mirrors the subset of `find` params
|
|
76
|
+
* the helper can honor against a `ls`-only store.
|
|
77
|
+
*/
|
|
78
|
+
export interface WalkViaLsOptions {
|
|
79
|
+
/**
|
|
80
|
+
* Glob pattern applied to URI tails (relative to `prefix`). Uses the
|
|
81
|
+
* v2 §3.3 grammar — `*`, `?`, and `**` all work. Patterns are matched
|
|
82
|
+
* against the post-prefix tail (same anchoring as `matchesGlob`).
|
|
83
|
+
* Defaults to "all descendants" (no pattern filter).
|
|
84
|
+
*/
|
|
85
|
+
pattern?: string;
|
|
86
|
+
/**
|
|
87
|
+
* `"full"` (default) returns `Output[]` — each `[uri, payload]` is
|
|
88
|
+
* the result of a separate `?fn=read` on that leaf. `"uris"` returns
|
|
89
|
+
* `string[]` — leaf URIs only, no payload reads.
|
|
90
|
+
*/
|
|
91
|
+
format?: "full" | "uris";
|
|
92
|
+
/**
|
|
93
|
+
* Soft cap on the number of distinct directories the helper will
|
|
94
|
+
* probe. Defaults to 10_000. When exceeded, the helper throws — the
|
|
95
|
+
* caller is almost certainly walking a larger tree than they meant
|
|
96
|
+
* to (or hitting a backend cycle). Set explicitly to opt into deeper
|
|
97
|
+
* walks.
|
|
98
|
+
*/
|
|
99
|
+
maxDirs?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Caller-invoked recursive `ls` walker — the polyfill for stores that
|
|
103
|
+
* advertise only `fn=ls`.
|
|
104
|
+
*
|
|
105
|
+
* @param read The b3nd `read` surface (`rig.read`, a connection's
|
|
106
|
+
* `read`, or any function with the same signature). Every
|
|
107
|
+
* call passes a single-element URL array; the helper never
|
|
108
|
+
* batches.
|
|
109
|
+
* @param prefix The routing prefix to walk. Trailing `/` is normalized
|
|
110
|
+
* in — callers can pass either form.
|
|
111
|
+
* @param opts See {@link WalkViaLsOptions}.
|
|
112
|
+
*
|
|
113
|
+
* @returns `Output[]` for `format=full` (default) or `string[]` for
|
|
114
|
+
* `format=uris`. The ordering is depth-first, in the order each
|
|
115
|
+
* probed directory's `ls` yields its children.
|
|
116
|
+
*
|
|
117
|
+
* @throws If a probed directory's `ls` rejects, the rejection
|
|
118
|
+
* propagates. If the per-walk directory cap is exceeded, throws
|
|
119
|
+
* `Error("walkViaLs: maxDirs exceeded")`.
|
|
120
|
+
*/
|
|
121
|
+
export declare function walkViaLs(read: <T = unknown>(urls: string[]) => Promise<Output<T>[]>, prefix: string, opts?: WalkViaLsOptions): Promise<Output[] | string[]>;
|
|
122
|
+
//# sourceMappingURL=walk-via-ls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk-via-ls.d.ts","sourceRoot":"","sources":["../src/walk-via-ls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kEAAkE,CAAC;AAG/F;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAC3D,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CA+E9B"}
|