@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/postgres/store.js
CHANGED
|
@@ -27,6 +27,27 @@
|
|
|
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
|
|
@@ -36,9 +57,10 @@ import { dispatchRead } from "../dispatch.js";
|
|
|
36
57
|
import { storageFailure } from "../errors.js";
|
|
37
58
|
import { toBytes } from "../payload.js";
|
|
38
59
|
import { validateReadParams } from "../read.js";
|
|
60
|
+
import { globToSqlLike } from "../glob.js";
|
|
61
|
+
import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
|
|
39
62
|
import { planColumns } from "./columns.js";
|
|
40
63
|
const STORE_NAME = "PostgresStore";
|
|
41
|
-
const TABLE_PREFIX = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
42
64
|
/** Map a `ColumnPlan.sqlType` to the `information_schema.columns.data_type` value. */
|
|
43
65
|
const INFO_SCHEMA_TYPE = {
|
|
44
66
|
TEXT: "text",
|
|
@@ -63,9 +85,25 @@ function rowToBytes(value) {
|
|
|
63
85
|
}
|
|
64
86
|
throw new Error(`${STORE_NAME}: unexpected payload type ${typeof value}`);
|
|
65
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Escape a literal value for inclusion in a SQL `LIKE` pattern. The
|
|
90
|
+
* surrounding query MUST declare `ESCAPE '\\'` for this to apply. The
|
|
91
|
+
* three SQL-LIKE metacharacters are escaped:
|
|
92
|
+
* - `%` → `\%`
|
|
93
|
+
* - `_` → `\_`
|
|
94
|
+
* - `\` → `\\` (escape char itself)
|
|
95
|
+
*
|
|
96
|
+
* Without this, a URI prefix carrying `%` or `_` would broaden the
|
|
97
|
+
* LIKE match — e.g. `store://r/100%/` would match every sibling under
|
|
98
|
+
* `store://r/100x/...` (the `%` would act as wildcard inside the
|
|
99
|
+
* concatenated pattern `prefix || '%'`).
|
|
100
|
+
*/
|
|
101
|
+
function escapeSqlLike(value) {
|
|
102
|
+
return value.replace(/[\\%_]/g, "\\$&");
|
|
103
|
+
}
|
|
66
104
|
function entityTableName(tablePrefix, entityName) {
|
|
67
|
-
if (!
|
|
68
|
-
throw new Error(`${STORE_NAME}: entity name '${entityName}' must match ${
|
|
105
|
+
if (!isValidIdentifier(entityName)) {
|
|
106
|
+
throw new Error(`${STORE_NAME}: entity name '${entityName}' must match ${IDENTIFIER_PATTERN.source}`);
|
|
69
107
|
}
|
|
70
108
|
return `${tablePrefix}_${entityName}_data`;
|
|
71
109
|
}
|
|
@@ -75,8 +113,8 @@ export class PostgresStore {
|
|
|
75
113
|
constructor(tablePrefix, executor) {
|
|
76
114
|
if (!tablePrefix)
|
|
77
115
|
throw new Error("tablePrefix is required");
|
|
78
|
-
if (!
|
|
79
|
-
throw new Error(`tablePrefix must match ${
|
|
116
|
+
if (!isValidIdentifier(tablePrefix)) {
|
|
117
|
+
throw new Error(`tablePrefix must match ${IDENTIFIER_PATTERN.source}; got '${tablePrefix}'`);
|
|
80
118
|
}
|
|
81
119
|
if (!executor)
|
|
82
120
|
throw new Error("executor is required");
|
|
@@ -212,6 +250,12 @@ CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);
|
|
|
212
250
|
read: (p) => this._readOne(meta, p.uri),
|
|
213
251
|
ls: (p) => this._ls(meta, p),
|
|
214
252
|
count: (p) => this._count(meta, p),
|
|
253
|
+
find: (p) => this._find(meta, p),
|
|
254
|
+
pushDownPattern: true,
|
|
255
|
+
pushDownCursor: true,
|
|
256
|
+
pushDownSortBy: true,
|
|
257
|
+
pushDownFind: true,
|
|
258
|
+
pushDownCount: true,
|
|
215
259
|
});
|
|
216
260
|
}
|
|
217
261
|
async delete(meta, uris) {
|
|
@@ -236,7 +280,7 @@ CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);
|
|
|
236
280
|
return {
|
|
237
281
|
status: "healthy",
|
|
238
282
|
message: "PostgreSQL store is operational",
|
|
239
|
-
fns: ["read", "ls", "count"],
|
|
283
|
+
fns: ["read", "ls", "find", "count"],
|
|
240
284
|
details: { tablePrefix: this.tablePrefix },
|
|
241
285
|
};
|
|
242
286
|
}
|
|
@@ -244,7 +288,7 @@ CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);
|
|
|
244
288
|
return {
|
|
245
289
|
status: "unhealthy",
|
|
246
290
|
message: `PostgreSQL health check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
247
|
-
fns: ["read", "ls", "count"],
|
|
291
|
+
fns: ["read", "ls", "find", "count"],
|
|
248
292
|
};
|
|
249
293
|
}
|
|
250
294
|
}
|
|
@@ -265,6 +309,31 @@ CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);
|
|
|
265
309
|
return adaptRowForRead(meta, res.rows[0]);
|
|
266
310
|
}
|
|
267
311
|
async _ls(meta, parsed) {
|
|
312
|
+
return await this._listQuery(meta, parsed, /*deep*/ false);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* `fn=find` handler — deep walk under the prefix. Same query shape as
|
|
316
|
+
* `_ls` minus the `NOT LIKE prefix || '%/%'` shallow clause: every
|
|
317
|
+
* descendant URI matches the prefix; the URI-embedded glob (passed in
|
|
318
|
+
* via `params.pattern` by the parser) tightens the set with a SQL
|
|
319
|
+
* `LIKE` body. `pushDownFind: true` is declared so dispatch trusts the
|
|
320
|
+
* backend with pattern + pagination + sort.
|
|
321
|
+
*/
|
|
322
|
+
async _find(meta, parsed) {
|
|
323
|
+
return await this._listQuery(meta, parsed, /*deep*/ true);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Shared SELECT for `_ls` and `_find`. The only structural difference
|
|
327
|
+
* is the shallow clause: `_ls` includes `uri NOT LIKE prefix || '%/%'
|
|
328
|
+
* ESCAPE '\\'`; `_find` drops it. Everything else — pattern body,
|
|
329
|
+
* cursor, sort, pagination — is identical.
|
|
330
|
+
*
|
|
331
|
+
* LIKE-injection safety: `parsed.uri` is pre-escaped with
|
|
332
|
+
* `escapeSqlLike()` before being bound. The bind value is the escaped
|
|
333
|
+
* prefix; the surrounding `LIKE $1 || '%' ESCAPE '\\'` then treats
|
|
334
|
+
* any `%`/`_` in the original prefix as literals.
|
|
335
|
+
*/
|
|
336
|
+
async _listQuery(meta, parsed, deep) {
|
|
268
337
|
validateReadParams(parsed.params, STORE_NAME);
|
|
269
338
|
const { params } = parsed;
|
|
270
339
|
const format = params.format ?? "full";
|
|
@@ -272,11 +341,43 @@ CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);
|
|
|
272
341
|
? "uri, " + meta.columns.map((c) => `"${c.name}"`).join(", ")
|
|
273
342
|
: "uri";
|
|
274
343
|
const selectClause = format === "uris" ? "uri" : cols;
|
|
275
|
-
|
|
276
|
-
|
|
344
|
+
// sortBy: "uri" sorts on the implicit URI column; any other value
|
|
345
|
+
// must be a declared user field, or dispatch will have already
|
|
346
|
+
// post-sorted and we should not double-sort here (dispatch strips
|
|
347
|
+
// non-uri sortBy from the handler call unless pushDownSortBy is
|
|
348
|
+
// true — which it is for this backend).
|
|
349
|
+
const sortByCol = params.sortBy === "uri"
|
|
350
|
+
? "uri"
|
|
351
|
+
: (params.sortBy !== undefined && meta.declared.has(params.sortBy)
|
|
352
|
+
? `"${params.sortBy}"`
|
|
353
|
+
: "");
|
|
354
|
+
const order = sortByCol
|
|
355
|
+
? ` ORDER BY ${sortByCol} ${params.sortOrder === "desc" ? "DESC" : "ASC"}`
|
|
277
356
|
: "";
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
// `$1` is the escaped prefix; `ESCAPE '\\'` is declared on every
|
|
358
|
+
// LIKE clause that uses it so embedded `%`/`_` in the URI stay
|
|
359
|
+
// literal. The shallow `NOT LIKE prefix || '%/%'` clause is dropped
|
|
360
|
+
// for the find (deep) path. When `pattern` is set we tighten with
|
|
361
|
+
// an additional `LIKE prefix || patternBody` clause (the body is
|
|
362
|
+
// produced by `globToSqlLike` and already escapes its own
|
|
363
|
+
// metacharacters). When `cursor` is set we add `uri > cursor` (or
|
|
364
|
+
// `<` for desc) — cursor goes into a comparison op, not LIKE, so
|
|
365
|
+
// no escaping needed there.
|
|
366
|
+
let sql = `SELECT ${selectClause} FROM ${meta.tableName} WHERE uri LIKE $1 || '%' ESCAPE '\\'`;
|
|
367
|
+
if (!deep) {
|
|
368
|
+
sql += ` AND uri NOT LIKE $1 || '%/%' ESCAPE '\\'`;
|
|
369
|
+
}
|
|
370
|
+
const args = [escapeSqlLike(parsed.uri)];
|
|
371
|
+
if (params.pattern !== undefined) {
|
|
372
|
+
args.push(globToSqlLike(params.pattern));
|
|
373
|
+
sql += ` AND uri LIKE $1 || $${args.length} ESCAPE '\\'`;
|
|
374
|
+
}
|
|
375
|
+
if (params.cursor !== undefined) {
|
|
376
|
+
args.push(params.cursor);
|
|
377
|
+
const op = params.sortOrder === "desc" ? "<" : ">";
|
|
378
|
+
sql += ` AND uri ${op} $${args.length}`;
|
|
379
|
+
}
|
|
380
|
+
sql += order;
|
|
280
381
|
if (params.limit !== undefined) {
|
|
281
382
|
const page = params.page ?? 1;
|
|
282
383
|
args.push(params.limit, (page - 1) * params.limit);
|
|
@@ -291,11 +392,30 @@ CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);
|
|
|
291
392
|
adaptRowForRead(meta, r),
|
|
292
393
|
]);
|
|
293
394
|
}
|
|
395
|
+
/**
|
|
396
|
+
* `fn=count` handler. Honours both shallow (`*`/`?` or no glob) and
|
|
397
|
+
* deep (`**`) URL shapes — the latter drops the `NOT LIKE %/%`
|
|
398
|
+
* predicate so descendants at any depth get counted. With
|
|
399
|
+
* `pushDownCount: true`, dispatch routes every count call through
|
|
400
|
+
* here (no walk-then-length fallback).
|
|
401
|
+
*/
|
|
294
402
|
async _count(meta, parsed) {
|
|
403
|
+
const deep = parsed.glob.includes("**");
|
|
404
|
+
let sql = `SELECT COUNT(*)::int AS n FROM ${meta.tableName} WHERE uri LIKE $1 || '%' ESCAPE '\\'`;
|
|
405
|
+
if (!deep) {
|
|
406
|
+
sql += ` AND uri NOT LIKE $1 || '%/%' ESCAPE '\\'`;
|
|
407
|
+
}
|
|
408
|
+
const args = [escapeSqlLike(parsed.uri)];
|
|
295
409
|
if (parsed.params.pattern !== undefined) {
|
|
296
|
-
|
|
410
|
+
args.push(globToSqlLike(parsed.params.pattern));
|
|
411
|
+
sql += ` AND uri LIKE $1 || $${args.length} ESCAPE '\\'`;
|
|
297
412
|
}
|
|
298
|
-
|
|
413
|
+
if (parsed.params.cursor !== undefined) {
|
|
414
|
+
args.push(parsed.params.cursor);
|
|
415
|
+
const op = parsed.params.sortOrder === "desc" ? "<" : ">";
|
|
416
|
+
sql += ` AND uri ${op} $${args.length}`;
|
|
417
|
+
}
|
|
418
|
+
const res = await this.executor.query(sql, args);
|
|
299
419
|
const row = res.rows?.[0];
|
|
300
420
|
return row?.n ?? 0;
|
|
301
421
|
}
|
package/esm/read.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-process post-processing for `fn=ls` results.
|
|
2
|
+
* In-process post-processing for `fn=ls`/`fn=find` results.
|
|
3
3
|
*
|
|
4
4
|
* Stores that cannot push `sortBy`/`limit`/`page`/`format` down to
|
|
5
5
|
* their backend collect the raw `Output[]` rows under a prefix and
|
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Throws on unsupported params — programmer errors are not silent
|
|
11
11
|
* "misses." See project decisions in `project_core_upgrade.md`.
|
|
12
|
+
*
|
|
13
|
+
* Glob compilation lives in `./glob.ts` — see `compileSaveGlob`,
|
|
14
|
+
* `globToSqlLike`, and `matchesGlob` for the regex/SQL adapters. This
|
|
15
|
+
* module no longer maintains its own glob grammar; the v2 listing
|
|
16
|
+
* spec (`.cc-chat/20260625121936-grammar-shape/output.md`) collapses
|
|
17
|
+
* read+observe onto one grammar — see §3.3.1 for the foundation-round
|
|
18
|
+
* wrapper resolution.
|
|
12
19
|
*/
|
|
13
|
-
import type { Output } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
20
|
+
import type { Output } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
14
21
|
import type { ReadParams } from "./url.js";
|
|
15
22
|
/**
|
|
16
23
|
* Validate standard ReadParams and throw on anything we cannot honor.
|
|
@@ -20,10 +27,17 @@ import type { ReadParams } from "./url.js";
|
|
|
20
27
|
* Stores that post-process in memory call `applyReadParams` instead,
|
|
21
28
|
* which validates and applies in one go.
|
|
22
29
|
*
|
|
23
|
-
* Project-wide baseline:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
30
|
+
* Project-wide baseline:
|
|
31
|
+
* - `sortBy` accepts `"uri"` (push-down everywhere), `"leaf"` (basename
|
|
32
|
+
* `localeCompare`), or any record field name (dispatch-layer post-sort,
|
|
33
|
+
* or backend push-down where advertised)
|
|
34
|
+
* - `format` only accepts `"full"` (default) or `"uris"`
|
|
35
|
+
* - `pattern` is supported as a glob over the URI tail
|
|
36
|
+
* - `cursor` is supported as a stateless continuation token (the URI
|
|
37
|
+
* of the last entry from a previous page); combining with `page` is
|
|
38
|
+
* a programmer error since the two are alternative pagination modes
|
|
39
|
+
*
|
|
40
|
+
* Per-store relaxations should be added explicitly as features land.
|
|
27
41
|
*/
|
|
28
42
|
export declare function validateReadParams(params: ReadParams, storeName: string): void;
|
|
29
43
|
/**
|
|
@@ -34,7 +48,40 @@ export declare function validateReadParams(params: ReadParams, storeName: string
|
|
|
34
48
|
* @param storeName label used in thrown error messages
|
|
35
49
|
*
|
|
36
50
|
* Returns `Output[]` when `format` is `"full"` (default) or `string[]`
|
|
37
|
-
* when `format` is `"uris"`.
|
|
51
|
+
* when `format` is `"uris"`. When `params.fields` is set and `format`
|
|
52
|
+
* is `"full"`, each row's payload is projected via `projectRecord` —
|
|
53
|
+
* unknown projection field names are silently absent.
|
|
54
|
+
*
|
|
55
|
+
* `sortBy` values:
|
|
56
|
+
* - `"uri"` — `localeCompare` of the full URI
|
|
57
|
+
* - `"leaf"` — `localeCompare` of the basename
|
|
58
|
+
* (`uri.slice(uri.lastIndexOf("/") + 1)`)
|
|
59
|
+
* - any other string — treated as a record-field name; non-record
|
|
60
|
+
* payloads sort last (`undefined`)
|
|
38
61
|
*/
|
|
39
62
|
export declare function applyReadParams<T>(rows: Output<T>[], params: ReadParams, storeName: string): Output<T>[] | string[];
|
|
63
|
+
/**
|
|
64
|
+
* Basename of a URI — everything after the last `/`. For URIs without a
|
|
65
|
+
* `/` (rare in save context but possible for opaque protocols) returns
|
|
66
|
+
* the whole string. Used by `sortBy=leaf` and exported for stores that
|
|
67
|
+
* push that sort down to a backend that needs a precomputed leaf
|
|
68
|
+
* column.
|
|
69
|
+
*/
|
|
70
|
+
export declare function leafOf(uri: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Stable, type-aware comparator: nulls/undefineds sort last; numbers
|
|
73
|
+
* and bigints compare numerically; dates compare by `valueOf()`; the
|
|
74
|
+
* rest falls back to string-coerced `localeCompare` so the comparator
|
|
75
|
+
* never throws on heterogeneous field values.
|
|
76
|
+
*/
|
|
77
|
+
export declare function compareSortable(a: unknown, b: unknown): number;
|
|
78
|
+
/**
|
|
79
|
+
* Project an `EntityRecord` down to the named fields. Unknown field
|
|
80
|
+
* names are silently absent — projection is a presentation directive,
|
|
81
|
+
* not a validation. Non-record values (eg `undefined` from a read
|
|
82
|
+
* miss, or a raw `Uint8Array` from a BYTES_ENTITY) pass through
|
|
83
|
+
* unchanged so callers can still detect misses and the BYTES path
|
|
84
|
+
* keeps its current shape.
|
|
85
|
+
*/
|
|
86
|
+
export declare function projectRecord<T>(value: T, fields: readonly string[]): T;
|
|
40
87
|
//# sourceMappingURL=read.d.ts.map
|
package/esm/read.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kEAAkE,CAAC;AAE/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,GAChB,IAAI,CAUN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,EACjB,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,GAChB,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CA8CxB;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG1C;AAgBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAiB9D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,CAYvE"}
|
package/esm/read.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-process post-processing for `fn=ls` results.
|
|
2
|
+
* In-process post-processing for `fn=ls`/`fn=find` results.
|
|
3
3
|
*
|
|
4
4
|
* Stores that cannot push `sortBy`/`limit`/`page`/`format` down to
|
|
5
5
|
* their backend collect the raw `Output[]` rows under a prefix and
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Throws on unsupported params — programmer errors are not silent
|
|
11
11
|
* "misses." See project decisions in `project_core_upgrade.md`.
|
|
12
|
+
*
|
|
13
|
+
* Glob compilation lives in `./glob.ts` — see `compileSaveGlob`,
|
|
14
|
+
* `globToSqlLike`, and `matchesGlob` for the regex/SQL adapters. This
|
|
15
|
+
* module no longer maintains its own glob grammar; the v2 listing
|
|
16
|
+
* spec (`.cc-chat/20260625121936-grammar-shape/output.md`) collapses
|
|
17
|
+
* read+observe onto one grammar — see §3.3.1 for the foundation-round
|
|
18
|
+
* wrapper resolution.
|
|
12
19
|
*/
|
|
13
20
|
/**
|
|
14
21
|
* Validate standard ReadParams and throw on anything we cannot honor.
|
|
@@ -18,20 +25,21 @@
|
|
|
18
25
|
* Stores that post-process in memory call `applyReadParams` instead,
|
|
19
26
|
* which validates and applies in one go.
|
|
20
27
|
*
|
|
21
|
-
* Project-wide baseline:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
28
|
+
* Project-wide baseline:
|
|
29
|
+
* - `sortBy` accepts `"uri"` (push-down everywhere), `"leaf"` (basename
|
|
30
|
+
* `localeCompare`), or any record field name (dispatch-layer post-sort,
|
|
31
|
+
* or backend push-down where advertised)
|
|
32
|
+
* - `format` only accepts `"full"` (default) or `"uris"`
|
|
33
|
+
* - `pattern` is supported as a glob over the URI tail
|
|
34
|
+
* - `cursor` is supported as a stateless continuation token (the URI
|
|
35
|
+
* of the last entry from a previous page); combining with `page` is
|
|
36
|
+
* a programmer error since the two are alternative pagination modes
|
|
37
|
+
*
|
|
38
|
+
* Per-store relaxations should be added explicitly as features land.
|
|
25
39
|
*/
|
|
26
40
|
export function validateReadParams(params, storeName) {
|
|
27
|
-
if (params.
|
|
28
|
-
throw new Error(`${storeName}:
|
|
29
|
-
}
|
|
30
|
-
if (params.cursor !== undefined) {
|
|
31
|
-
throw new Error(`${storeName}: cursor not supported`);
|
|
32
|
-
}
|
|
33
|
-
if (params.sortBy !== undefined && params.sortBy !== "uri") {
|
|
34
|
-
throw new Error(`${storeName}: unsupported sortBy: ${params.sortBy}`);
|
|
41
|
+
if (params.cursor !== undefined && params.page !== undefined) {
|
|
42
|
+
throw new Error(`${storeName}: cursor and page cannot be combined — pick one pagination mode`);
|
|
35
43
|
}
|
|
36
44
|
const format = params.format ?? "full";
|
|
37
45
|
if (format !== "full" && format !== "uris") {
|
|
@@ -46,15 +54,42 @@ export function validateReadParams(params, storeName) {
|
|
|
46
54
|
* @param storeName label used in thrown error messages
|
|
47
55
|
*
|
|
48
56
|
* Returns `Output[]` when `format` is `"full"` (default) or `string[]`
|
|
49
|
-
* when `format` is `"uris"`.
|
|
57
|
+
* when `format` is `"uris"`. When `params.fields` is set and `format`
|
|
58
|
+
* is `"full"`, each row's payload is projected via `projectRecord` —
|
|
59
|
+
* unknown projection field names are silently absent.
|
|
60
|
+
*
|
|
61
|
+
* `sortBy` values:
|
|
62
|
+
* - `"uri"` — `localeCompare` of the full URI
|
|
63
|
+
* - `"leaf"` — `localeCompare` of the basename
|
|
64
|
+
* (`uri.slice(uri.lastIndexOf("/") + 1)`)
|
|
65
|
+
* - any other string — treated as a record-field name; non-record
|
|
66
|
+
* payloads sort last (`undefined`)
|
|
50
67
|
*/
|
|
51
68
|
export function applyReadParams(rows, params, storeName) {
|
|
52
69
|
validateReadParams(params, storeName);
|
|
53
70
|
const format = params.format ?? "full";
|
|
54
71
|
let out = rows;
|
|
55
|
-
if (params.sortBy
|
|
72
|
+
if (params.sortBy !== undefined) {
|
|
56
73
|
const dir = params.sortOrder === "desc" ? -1 : 1;
|
|
57
|
-
|
|
74
|
+
if (params.sortBy === "uri") {
|
|
75
|
+
out = [...out].sort(([a], [b]) => a.localeCompare(b) * dir);
|
|
76
|
+
}
|
|
77
|
+
else if (params.sortBy === "leaf") {
|
|
78
|
+
out = [...out].sort(([a], [b]) => leafOf(a).localeCompare(leafOf(b)) * dir);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const field = params.sortBy;
|
|
82
|
+
out = [...out].sort(([, a], [, b]) => {
|
|
83
|
+
const av = recordField(a, field);
|
|
84
|
+
const bv = recordField(b, field);
|
|
85
|
+
return compareSortable(av, bv) * dir;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (params.cursor !== undefined) {
|
|
90
|
+
const cursor = params.cursor;
|
|
91
|
+
const desc = params.sortOrder === "desc";
|
|
92
|
+
out = out.filter(([uri]) => desc ? uri.localeCompare(cursor) < 0 : uri.localeCompare(cursor) > 0);
|
|
58
93
|
}
|
|
59
94
|
if (params.limit !== undefined) {
|
|
60
95
|
const page = params.page ?? 1;
|
|
@@ -63,5 +98,95 @@ export function applyReadParams(rows, params, storeName) {
|
|
|
63
98
|
}
|
|
64
99
|
if (format === "uris")
|
|
65
100
|
return out.map(([uri]) => uri);
|
|
101
|
+
if (params.fields && params.fields.length > 0) {
|
|
102
|
+
const allow = params.fields;
|
|
103
|
+
return out.map(([uri, payload]) => [
|
|
104
|
+
uri,
|
|
105
|
+
projectRecord(payload, allow),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Basename of a URI — everything after the last `/`. For URIs without a
|
|
112
|
+
* `/` (rare in save context but possible for opaque protocols) returns
|
|
113
|
+
* the whole string. Used by `sortBy=leaf` and exported for stores that
|
|
114
|
+
* push that sort down to a backend that needs a precomputed leaf
|
|
115
|
+
* column.
|
|
116
|
+
*/
|
|
117
|
+
export function leafOf(uri) {
|
|
118
|
+
const idx = uri.lastIndexOf("/");
|
|
119
|
+
return idx < 0 ? uri : uri.slice(idx + 1);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Read a named field from a payload, returning `undefined` for any
|
|
123
|
+
* non-record payload (eg `Uint8Array`, `ReadableStream`, miss). Used
|
|
124
|
+
* by the in-memory sort path so non-uri sortBy works against
|
|
125
|
+
* `EntityRecord` rows.
|
|
126
|
+
*/
|
|
127
|
+
function recordField(value, field) {
|
|
128
|
+
if (value === undefined || value === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (typeof value !== "object")
|
|
131
|
+
return undefined;
|
|
132
|
+
if (value instanceof Uint8Array)
|
|
133
|
+
return undefined;
|
|
134
|
+
if (value instanceof ReadableStream)
|
|
135
|
+
return undefined;
|
|
136
|
+
return value[field];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Stable, type-aware comparator: nulls/undefineds sort last; numbers
|
|
140
|
+
* and bigints compare numerically; dates compare by `valueOf()`; the
|
|
141
|
+
* rest falls back to string-coerced `localeCompare` so the comparator
|
|
142
|
+
* never throws on heterogeneous field values.
|
|
143
|
+
*/
|
|
144
|
+
export function compareSortable(a, b) {
|
|
145
|
+
const aMissing = a === undefined || a === null;
|
|
146
|
+
const bMissing = b === undefined || b === null;
|
|
147
|
+
if (aMissing && bMissing)
|
|
148
|
+
return 0;
|
|
149
|
+
if (aMissing)
|
|
150
|
+
return 1;
|
|
151
|
+
if (bMissing)
|
|
152
|
+
return -1;
|
|
153
|
+
if (typeof a === "number" && typeof b === "number")
|
|
154
|
+
return a - b;
|
|
155
|
+
if (typeof a === "bigint" && typeof b === "bigint") {
|
|
156
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
157
|
+
}
|
|
158
|
+
if (a instanceof Date && b instanceof Date) {
|
|
159
|
+
return a.valueOf() - b.valueOf();
|
|
160
|
+
}
|
|
161
|
+
if (typeof a === "boolean" && typeof b === "boolean") {
|
|
162
|
+
return (a ? 1 : 0) - (b ? 1 : 0);
|
|
163
|
+
}
|
|
164
|
+
return String(a).localeCompare(String(b));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Project an `EntityRecord` down to the named fields. Unknown field
|
|
168
|
+
* names are silently absent — projection is a presentation directive,
|
|
169
|
+
* not a validation. Non-record values (eg `undefined` from a read
|
|
170
|
+
* miss, or a raw `Uint8Array` from a BYTES_ENTITY) pass through
|
|
171
|
+
* unchanged so callers can still detect misses and the BYTES path
|
|
172
|
+
* keeps its current shape.
|
|
173
|
+
*/
|
|
174
|
+
export function projectRecord(value, fields) {
|
|
175
|
+
if (value === undefined || value === null)
|
|
176
|
+
return value;
|
|
177
|
+
if (typeof value !== "object")
|
|
178
|
+
return value;
|
|
179
|
+
if (value instanceof Uint8Array)
|
|
180
|
+
return value;
|
|
181
|
+
if (value instanceof ReadableStream)
|
|
182
|
+
return value;
|
|
183
|
+
if (Array.isArray(value))
|
|
184
|
+
return value;
|
|
185
|
+
const out = {};
|
|
186
|
+
const src = value;
|
|
187
|
+
for (const f of fields) {
|
|
188
|
+
if (Object.prototype.hasOwnProperty.call(src, f))
|
|
189
|
+
out[f] = src[f];
|
|
190
|
+
}
|
|
66
191
|
return out;
|
|
67
192
|
}
|