@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/sqlite/store.js
CHANGED
|
@@ -1,20 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SqliteStore — SQLite implementation of
|
|
2
|
+
* SqliteStore — SQLite implementation of `EntityStore`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* One layout for every schema: `{prefix}_{entity}_data` with `uri TEXT
|
|
5
|
+
* PRIMARY KEY` and one column per supported field, typed by the
|
|
6
|
+
* canonical `TYPE_TAGS` it carries (see `./columns.ts`). `BYTES_ENTITY`
|
|
7
|
+
* is just an entity with one `payload BLOB` column — no legacy
|
|
8
|
+
* special case.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* is
|
|
10
|
+
* ## Lifecycle
|
|
11
|
+
*
|
|
12
|
+
* `entitySupport(schema)` is pure — it runs `planColumns` and returns
|
|
13
|
+
* a `SqliteEntityMeta` with the target table name, column plans,
|
|
14
|
+
* bytes-column set, and the support report. No IO.
|
|
15
|
+
*
|
|
16
|
+
* `provisionEntity(meta)` runs `CREATE TABLE IF NOT EXISTS` and then
|
|
17
|
+
* verifies the live shape matches the meta (catches same-name-
|
|
18
|
+
* different-shape collisions). `entityStatus(meta)` performs the same
|
|
19
|
+
* check in isolation via `PRAGMA table_info`.
|
|
20
|
+
*
|
|
21
|
+
* Writes/reads/deletes consume the meta directly: no per-call cache
|
|
22
|
+
* gate. Operations against a not-yet-provisioned meta surface
|
|
23
|
+
* SQLite's natural failure per entry.
|
|
24
|
+
*
|
|
25
|
+
* `fn=ls` / `fn=count` push down to SQL — the shallow-direct-leaves
|
|
26
|
+
* predicate (`uri LIKE prefix || '%' AND uri NOT LIKE prefix || '%/%'`)
|
|
27
|
+
* is enforced in the WHERE clause against the entity's table.
|
|
28
|
+
*
|
|
29
|
+
* `fn=find` (v2 spec §3.5; foundation PR #82) reuses the same `_ls`
|
|
30
|
+
* SQL composition with a single branch: under `fn=find` the shallow
|
|
31
|
+
* safety predicate (`AND uri NOT LIKE prefix || '%/%'`) is DROPPED so
|
|
32
|
+
* the LIKE prefix walk crosses `/` and matches the whole subtree.
|
|
33
|
+
* The prefix bind is still a two-arg `[prefix, globToSqlLike(pattern)]`
|
|
34
|
+
* pair — never concatenated into the pattern body, so room/segment
|
|
35
|
+
* names containing literal `%`/`_` cannot break out of the parameter
|
|
36
|
+
* (LIKE-injection safety via `ESCAPE '\'`). `pushDownFind: true` is
|
|
37
|
+
* advertised so dispatch trusts the backend's deep walk.
|
|
38
|
+
*
|
|
39
|
+
* Writes are wrapped in a transaction (`capabilities.atomicBatch =
|
|
40
|
+
* true`). Stream-shaped values on `BLOB` columns are collected to
|
|
41
|
+
* `Uint8Array` *before* the transaction opens.
|
|
11
42
|
*/
|
|
12
|
-
import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
|
|
13
43
|
import { dispatchRead } from "../dispatch.js";
|
|
14
44
|
import { storageFailure } from "../errors.js";
|
|
15
45
|
import { toBytes } from "../payload.js";
|
|
16
46
|
import { validateReadParams } from "../read.js";
|
|
47
|
+
import { globToSqlLike } from "../glob.js";
|
|
48
|
+
import { TYPE_TAGS, } from "../entity.js";
|
|
49
|
+
import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
|
|
50
|
+
import { planColumns } from "./columns.js";
|
|
17
51
|
const STORE_NAME = "SqliteStore";
|
|
52
|
+
/**
|
|
53
|
+
* Escape the SQL `LIKE` metacharacters (`%`, `_`, `\`) in a literal
|
|
54
|
+
* prefix or substring so the value can be safely bound into a `LIKE ?`
|
|
55
|
+
* clause paired with `ESCAPE '\'`. Without this, a prefix carrying a
|
|
56
|
+
* literal `%` (URL-encoded byte segment) or `_` (snake_case segment)
|
|
57
|
+
* would expand to a SQL wildcard inside the bind value — a
|
|
58
|
+
* LIKE-injection that parameterisation alone does NOT block.
|
|
59
|
+
*
|
|
60
|
+
* Used by `_ls` and `_count` for every prefix bind so room/segment
|
|
61
|
+
* names containing `%`/`_` stay inert.
|
|
62
|
+
*/
|
|
63
|
+
function escapeSqlLike(value) {
|
|
64
|
+
return value
|
|
65
|
+
.replace(/\\/g, "\\\\")
|
|
66
|
+
.replace(/%/g, "\\%")
|
|
67
|
+
.replace(/_/g, "\\_");
|
|
68
|
+
}
|
|
18
69
|
function rowToBytes(value) {
|
|
19
70
|
if (value instanceof Uint8Array)
|
|
20
71
|
return value;
|
|
@@ -22,129 +73,168 @@ function rowToBytes(value) {
|
|
|
22
73
|
return new Uint8Array(value);
|
|
23
74
|
if (Array.isArray(value))
|
|
24
75
|
return new Uint8Array(value);
|
|
25
|
-
throw new Error(
|
|
76
|
+
throw new Error(`${STORE_NAME}: unexpected payload type ${typeof value}`);
|
|
77
|
+
}
|
|
78
|
+
function entityTableName(tablePrefix, entityName) {
|
|
79
|
+
if (!isValidIdentifier(entityName)) {
|
|
80
|
+
throw new Error(`${STORE_NAME}: entity name '${entityName}' must match ${IDENTIFIER_PATTERN.source}`);
|
|
81
|
+
}
|
|
82
|
+
return `${tablePrefix}_${entityName}_data`;
|
|
26
83
|
}
|
|
27
84
|
export class SqliteStore {
|
|
28
|
-
|
|
85
|
+
tablePrefix;
|
|
29
86
|
executor;
|
|
30
|
-
// Names of entities provisioned on this store. SqliteStore currently
|
|
31
|
-
// routes only BYTES_ENTITY, so this is at most `{"bytes"}` — but the
|
|
32
|
-
// tracking shape matches MemoryStore so `status().schema` reads the
|
|
33
|
-
// same way across backends.
|
|
34
|
-
provisioned = new Set();
|
|
35
87
|
constructor(tablePrefix, executor) {
|
|
36
88
|
if (!tablePrefix)
|
|
37
89
|
throw new Error("tablePrefix is required");
|
|
90
|
+
if (!isValidIdentifier(tablePrefix)) {
|
|
91
|
+
throw new Error(`tablePrefix must match ${IDENTIFIER_PATTERN.source}; got '${tablePrefix}'`);
|
|
92
|
+
}
|
|
38
93
|
if (!executor)
|
|
39
94
|
throw new Error("executor is required");
|
|
40
|
-
this.
|
|
95
|
+
this.tablePrefix = tablePrefix;
|
|
41
96
|
this.executor = executor;
|
|
42
97
|
}
|
|
43
|
-
// ──
|
|
98
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
44
99
|
entitySupport(schema) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
const { columns, unsupported } = planColumns(schema.fields);
|
|
101
|
+
const support = {
|
|
102
|
+
entity: schema.name,
|
|
103
|
+
supported: columns.map((c) => c.name),
|
|
104
|
+
unsupported,
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
support,
|
|
108
|
+
tableName: entityTableName(this.tablePrefix, schema.name),
|
|
109
|
+
columns,
|
|
110
|
+
declared: new Set(columns.map((c) => c.name)),
|
|
111
|
+
bytesColumns: new Set(columns.filter((c) => c.sqlType === "BLOB").map((c) => c.name)),
|
|
112
|
+
};
|
|
58
113
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
// deno-lint-ignore require-await
|
|
115
|
+
async entityStatus(meta) {
|
|
116
|
+
const info = this.executor.query(`PRAGMA table_info("${meta.tableName}")`);
|
|
117
|
+
const rows = (info.rows ?? []);
|
|
118
|
+
if (rows.length === 0)
|
|
119
|
+
return "unprovisioned";
|
|
120
|
+
const actual = new Map(rows.map((r) => [r.name, (r.type ?? "").toUpperCase()]));
|
|
121
|
+
for (const c of meta.columns) {
|
|
122
|
+
const got = actual.get(c.name);
|
|
123
|
+
if (got === undefined)
|
|
124
|
+
return "unprovisioned";
|
|
125
|
+
if (got !== c.sqlType)
|
|
126
|
+
return "unprovisioned";
|
|
127
|
+
}
|
|
128
|
+
// Reject collisions in the other direction: extra user columns
|
|
129
|
+
// present in the live table that the meta doesn't declare are a
|
|
130
|
+
// different shape.
|
|
131
|
+
for (const name of actual.keys()) {
|
|
132
|
+
if (name === "uri" || name === "created_at" || name === "updated_at") {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!meta.declared.has(name))
|
|
136
|
+
return "unprovisioned";
|
|
137
|
+
}
|
|
138
|
+
return "live";
|
|
64
139
|
}
|
|
65
|
-
|
|
66
|
-
|
|
140
|
+
async provisionEntity(meta) {
|
|
141
|
+
const colDdl = meta.columns
|
|
142
|
+
.map((c) => `"${c.name}" ${c.sqlType}`)
|
|
143
|
+
.join(",\n ");
|
|
144
|
+
const sql = `CREATE TABLE IF NOT EXISTS "${meta.tableName}" (
|
|
145
|
+
uri TEXT PRIMARY KEY${meta.columns.length > 0 ? ",\n " + colDdl : ""},
|
|
146
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
147
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
148
|
+
);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS "idx_${meta.tableName}_uri" ON "${meta.tableName}" (uri);`;
|
|
150
|
+
// SQLite drivers vary on whether `query` accepts multi-statement
|
|
151
|
+
// SQL; split and run each.
|
|
152
|
+
for (const stmt of sql.split(";").map((s) => s.trim()).filter(Boolean)) {
|
|
153
|
+
this.executor.query(stmt);
|
|
154
|
+
}
|
|
155
|
+
const status = await this.entityStatus(meta);
|
|
156
|
+
if (status !== "live") {
|
|
157
|
+
throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
|
|
158
|
+
`provisioned with a different shape at table '${meta.tableName}'`);
|
|
159
|
+
}
|
|
67
160
|
}
|
|
68
|
-
// ──
|
|
69
|
-
async
|
|
161
|
+
// ── Write ────────────────────────────────────────────────────────
|
|
162
|
+
async write(meta, entries) {
|
|
70
163
|
if (entries.length === 0)
|
|
71
164
|
return [];
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
165
|
+
const out = new Array(entries.length);
|
|
166
|
+
const accepted = [];
|
|
167
|
+
for (let i = 0; i < entries.length; i++) {
|
|
168
|
+
const { uri, record } = entries[i];
|
|
169
|
+
const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
|
|
170
|
+
if (extras.length > 0) {
|
|
171
|
+
out[i] = {
|
|
172
|
+
success: false,
|
|
173
|
+
...storageFailure(new Error(`${STORE_NAME}: record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
|
|
174
|
+
};
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const normalised = await normaliseBytesFields(record, meta.bytesColumns);
|
|
179
|
+
accepted.push({ idx: i, uri, record: normalised });
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
out[i] = {
|
|
183
|
+
success: false,
|
|
184
|
+
...storageFailure(err, "Invalid record", uri),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (accepted.length === 0)
|
|
189
|
+
return out;
|
|
190
|
+
const cols = meta.columns;
|
|
191
|
+
const colList = cols.map((c) => `"${c.name}"`).join(", ");
|
|
192
|
+
const placeholders = cols.map(() => "?").join(", ");
|
|
193
|
+
const updates = cols
|
|
194
|
+
.map((c) => `"${c.name}" = excluded."${c.name}"`)
|
|
195
|
+
.concat(["updated_at = datetime('now')"])
|
|
196
|
+
.join(", ");
|
|
81
197
|
try {
|
|
82
198
|
this.executor.transaction((tx) => {
|
|
83
|
-
for (const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
199
|
+
for (const { uri, record } of accepted) {
|
|
200
|
+
const args = [uri];
|
|
201
|
+
for (const c of cols)
|
|
202
|
+
args.push(adaptForWrite(c, record[c.name]));
|
|
203
|
+
tx.query(`INSERT INTO "${meta.tableName}" (uri${colList ? ", " + colList : ""}) VALUES (?${placeholders ? ", " + placeholders : ""})
|
|
204
|
+
ON CONFLICT(uri) DO UPDATE SET ${updates}`, args);
|
|
89
205
|
}
|
|
90
206
|
});
|
|
91
|
-
|
|
207
|
+
for (const { idx } of accepted)
|
|
208
|
+
out[idx] = { success: true };
|
|
209
|
+
return out;
|
|
92
210
|
}
|
|
93
211
|
catch (err) {
|
|
94
212
|
const failure = storageFailure(err, "Write failed");
|
|
95
|
-
|
|
213
|
+
for (const { idx } of accepted)
|
|
214
|
+
out[idx] = { success: false, ...failure };
|
|
215
|
+
return out;
|
|
96
216
|
}
|
|
97
217
|
}
|
|
98
|
-
|
|
218
|
+
// ── Read ─────────────────────────────────────────────────────────
|
|
219
|
+
read(meta, urls) {
|
|
99
220
|
return dispatchRead(urls, STORE_NAME, {
|
|
100
|
-
read: (p) => this._readOne(p.uri),
|
|
101
|
-
ls: (p) => this._ls(p),
|
|
102
|
-
|
|
221
|
+
read: (p) => Promise.resolve(this._readOne(meta, p.uri)),
|
|
222
|
+
ls: (p) => Promise.resolve(this._ls(meta, p, false)),
|
|
223
|
+
find: (p) => Promise.resolve(this._ls(meta, p, true)),
|
|
224
|
+
count: (p) => Promise.resolve(this._count(meta, p)),
|
|
225
|
+
pushDownPattern: true,
|
|
226
|
+
pushDownCursor: true,
|
|
227
|
+
pushDownSortBy: true,
|
|
228
|
+
pushDownFind: true,
|
|
103
229
|
});
|
|
104
230
|
}
|
|
105
|
-
|
|
106
|
-
const result = this.executor.query(`SELECT payload FROM ${this.tableName} WHERE uri = ?`, [uri]);
|
|
107
|
-
if (!result.rows.length)
|
|
108
|
-
return undefined;
|
|
109
|
-
return rowToBytes(result.rows[0].payload);
|
|
110
|
-
}
|
|
111
|
-
_ls(parsed) {
|
|
112
|
-
validateReadParams(parsed.params, STORE_NAME);
|
|
113
|
-
const { params } = parsed;
|
|
114
|
-
const format = params.format ?? "full";
|
|
115
|
-
const select = format === "uris" ? "uri" : "uri, payload";
|
|
116
|
-
const order = params.sortBy === "uri"
|
|
117
|
-
? ` ORDER BY uri ${params.sortOrder === "desc" ? "DESC" : "ASC"}`
|
|
118
|
-
: "";
|
|
119
|
-
let sql = `SELECT ${select} FROM ${this.tableName} WHERE uri LIKE ? || '%' AND uri NOT LIKE ? || '%/%'${order}`;
|
|
120
|
-
const args = [parsed.uri, parsed.uri];
|
|
121
|
-
if (params.limit !== undefined) {
|
|
122
|
-
const page = params.page ?? 1;
|
|
123
|
-
sql += ` LIMIT ? OFFSET ?`;
|
|
124
|
-
args.push(params.limit, (page - 1) * params.limit);
|
|
125
|
-
}
|
|
126
|
-
const result = this.executor.query(sql, args);
|
|
127
|
-
const rows = result.rows;
|
|
128
|
-
if (format === "uris")
|
|
129
|
-
return rows.map((r) => r.uri);
|
|
130
|
-
return rows.map((r) => [r.uri, rowToBytes(r.payload)]);
|
|
131
|
-
}
|
|
132
|
-
_count(parsed) {
|
|
133
|
-
if (parsed.params.pattern !== undefined) {
|
|
134
|
-
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
135
|
-
}
|
|
136
|
-
const result = this.executor.query(`SELECT COUNT(*) AS n FROM ${this.tableName} WHERE uri LIKE ? || '%' AND uri NOT LIKE ? || '%/%'`, [parsed.uri, parsed.uri]);
|
|
137
|
-
const row = result.rows[0];
|
|
138
|
-
return row?.n ?? 0;
|
|
139
|
-
}
|
|
140
|
-
_deleteBytes(uris) {
|
|
231
|
+
delete(meta, uris) {
|
|
141
232
|
if (uris.length === 0)
|
|
142
233
|
return Promise.resolve([]);
|
|
143
|
-
// Atomic batch: every uri either deletes together or not at all.
|
|
144
234
|
try {
|
|
145
235
|
this.executor.transaction((tx) => {
|
|
146
236
|
for (const uri of uris) {
|
|
147
|
-
tx.query(`DELETE FROM ${
|
|
237
|
+
tx.query(`DELETE FROM "${meta.tableName}" WHERE uri = ?`, [uri]);
|
|
148
238
|
}
|
|
149
239
|
});
|
|
150
240
|
return Promise.resolve(uris.map(() => ({ success: true })));
|
|
@@ -154,31 +244,245 @@ export class SqliteStore {
|
|
|
154
244
|
return Promise.resolve(uris.map(() => ({ success: false, ...failure })));
|
|
155
245
|
}
|
|
156
246
|
}
|
|
157
|
-
// ── Status
|
|
247
|
+
// ── Status / capabilities ────────────────────────────────────────
|
|
158
248
|
status() {
|
|
159
249
|
try {
|
|
160
250
|
this.executor.query("SELECT 1");
|
|
161
251
|
return Promise.resolve({
|
|
162
252
|
status: "healthy",
|
|
163
253
|
message: "SQLite store is operational",
|
|
164
|
-
schema: this.
|
|
165
|
-
fns: ["read", "ls", "count"],
|
|
166
|
-
details: {
|
|
254
|
+
schema: this._listProvisionedEntities().map((n) => `entity:${n}`),
|
|
255
|
+
fns: ["read", "ls", "find", "count"],
|
|
256
|
+
details: { tablePrefix: this.tablePrefix },
|
|
167
257
|
});
|
|
168
258
|
}
|
|
169
259
|
catch (error) {
|
|
170
260
|
return Promise.resolve({
|
|
171
261
|
status: "unhealthy",
|
|
172
262
|
message: error instanceof Error ? error.message : String(error),
|
|
173
|
-
schema:
|
|
174
|
-
fns: ["read", "ls", "count"],
|
|
263
|
+
schema: [],
|
|
264
|
+
fns: ["read", "ls", "find", "count"],
|
|
175
265
|
});
|
|
176
266
|
}
|
|
177
267
|
}
|
|
178
|
-
schemaUris() {
|
|
179
|
-
return [...this.provisioned].map((name) => `entity:${name}`);
|
|
180
|
-
}
|
|
181
268
|
capabilities() {
|
|
182
269
|
return { atomicBatch: true };
|
|
183
270
|
}
|
|
271
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
272
|
+
_readOne(meta, uri) {
|
|
273
|
+
try {
|
|
274
|
+
if (meta.columns.length === 0) {
|
|
275
|
+
// Empty schema: presence-only — return an empty record on hit.
|
|
276
|
+
const res = this.executor.query(`SELECT 1 FROM "${meta.tableName}" WHERE uri = ?`, [uri]);
|
|
277
|
+
return res.rows && res.rows.length > 0 ? {} : undefined;
|
|
278
|
+
}
|
|
279
|
+
const cols = meta.columns.map((c) => `"${c.name}"`).join(", ");
|
|
280
|
+
const res = this.executor.query(`SELECT ${cols} FROM "${meta.tableName}" WHERE uri = ?`, [uri]);
|
|
281
|
+
if (!res.rows || res.rows.length === 0)
|
|
282
|
+
return undefined;
|
|
283
|
+
return adaptRowForRead(meta, res.rows[0]);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// `no such table` and other natural read failures surface as
|
|
287
|
+
// misses — matches the convention that unprovisioned entities
|
|
288
|
+
// read empty rather than throwing.
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
_ls(meta, parsed, deep) {
|
|
293
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
294
|
+
const { params } = parsed;
|
|
295
|
+
const format = params.format ?? "full";
|
|
296
|
+
const cols = meta.columns.length > 0
|
|
297
|
+
? "uri, " + meta.columns.map((c) => `"${c.name}"`).join(", ")
|
|
298
|
+
: "uri";
|
|
299
|
+
const selectClause = format === "uris" ? "uri" : cols;
|
|
300
|
+
// sortBy: "uri" sorts on the implicit URI column; any other value
|
|
301
|
+
// must be a declared user field (validated against meta.declared
|
|
302
|
+
// to keep the column name out of arbitrary user input).
|
|
303
|
+
const sortByCol = params.sortBy === "uri"
|
|
304
|
+
? "uri"
|
|
305
|
+
: (params.sortBy !== undefined && meta.declared.has(params.sortBy)
|
|
306
|
+
? `"${params.sortBy}"`
|
|
307
|
+
: "");
|
|
308
|
+
const order = sortByCol
|
|
309
|
+
? ` ORDER BY ${sortByCol} ${params.sortOrder === "desc" ? "DESC" : "ASC"}`
|
|
310
|
+
: "";
|
|
311
|
+
// Base prefix predicate. Under `fn=ls` (deep=false) the
|
|
312
|
+
// shallow-direct-leaves safety predicate `AND uri NOT LIKE
|
|
313
|
+
// prefix||'%/%'` is layered on top so the listing stops at depth-1.
|
|
314
|
+
// Under `fn=find` (deep=true) that predicate is dropped — find's
|
|
315
|
+
// entire purpose is to cross `/` and walk the subtree.
|
|
316
|
+
//
|
|
317
|
+
// LIKE-injection safety: SQLite parameterisation prevents SQL
|
|
318
|
+
// injection but the `%`/`_` characters carried in the bind VALUE
|
|
319
|
+
// are still treated as wildcards by LIKE. Room and segment names
|
|
320
|
+
// routinely include `%` (URL-encoded bytes) or `_` (snake_case),
|
|
321
|
+
// so we ESCAPE the prefix bind with `\` and pair every LIKE clause
|
|
322
|
+
// with `ESCAPE '\'`. Without the escape, a prefix
|
|
323
|
+
// `store://r/100%/` would match `store://r/100x/...` too. The
|
|
324
|
+
// pattern body is also escaped (by `globToSqlLike`) and uses the
|
|
325
|
+
// same `ESCAPE '\'`.
|
|
326
|
+
const prefixLike = escapeSqlLike(parsed.uri);
|
|
327
|
+
let sql = `SELECT ${selectClause} FROM "${meta.tableName}" WHERE uri LIKE ? || '%' ESCAPE '\\'`;
|
|
328
|
+
const args = [prefixLike];
|
|
329
|
+
if (!deep) {
|
|
330
|
+
sql += ` AND uri NOT LIKE ? || '%/%' ESCAPE '\\'`;
|
|
331
|
+
args.push(prefixLike);
|
|
332
|
+
}
|
|
333
|
+
if (params.pattern !== undefined) {
|
|
334
|
+
args.push(prefixLike, globToSqlLike(params.pattern));
|
|
335
|
+
sql += ` AND uri LIKE ? || ? ESCAPE '\\'`;
|
|
336
|
+
}
|
|
337
|
+
if (params.cursor !== undefined) {
|
|
338
|
+
args.push(params.cursor);
|
|
339
|
+
const op = params.sortOrder === "desc" ? "<" : ">";
|
|
340
|
+
sql += ` AND uri ${op} ?`;
|
|
341
|
+
}
|
|
342
|
+
sql += order;
|
|
343
|
+
if (params.limit !== undefined) {
|
|
344
|
+
const page = params.page ?? 1;
|
|
345
|
+
sql += ` LIMIT ? OFFSET ?`;
|
|
346
|
+
args.push(params.limit, (page - 1) * params.limit);
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const res = this.executor.query(sql, args);
|
|
350
|
+
const rows = (res.rows ?? []);
|
|
351
|
+
if (format === "uris")
|
|
352
|
+
return rows.map((r) => r.uri);
|
|
353
|
+
return rows.map((r) => [
|
|
354
|
+
r.uri,
|
|
355
|
+
adaptRowForRead(meta, r),
|
|
356
|
+
]);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return format === "uris" ? [] : [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
_count(meta, parsed) {
|
|
363
|
+
// Same LIKE-injection treatment as `_ls`: escape the prefix bind
|
|
364
|
+
// and pair every LIKE with `ESCAPE '\'` so literal `%`/`_` in
|
|
365
|
+
// room/segment names stay inert.
|
|
366
|
+
const prefixLike = escapeSqlLike(parsed.uri);
|
|
367
|
+
let sql = `SELECT COUNT(*) AS n FROM "${meta.tableName}" WHERE uri LIKE ? || '%' ESCAPE '\\' AND uri NOT LIKE ? || '%/%' ESCAPE '\\'`;
|
|
368
|
+
const args = [prefixLike, prefixLike];
|
|
369
|
+
if (parsed.params.pattern !== undefined) {
|
|
370
|
+
args.push(prefixLike, globToSqlLike(parsed.params.pattern));
|
|
371
|
+
sql += ` AND uri LIKE ? || ? ESCAPE '\\'`;
|
|
372
|
+
}
|
|
373
|
+
if (parsed.params.cursor !== undefined) {
|
|
374
|
+
args.push(parsed.params.cursor);
|
|
375
|
+
const op = parsed.params.sortOrder === "desc" ? "<" : ">";
|
|
376
|
+
sql += ` AND uri ${op} ?`;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const res = this.executor.query(sql, args);
|
|
380
|
+
const row = res.rows?.[0];
|
|
381
|
+
return row?.n ?? 0;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return 0;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Enumerate every `{tablePrefix}_*_data` table currently in the
|
|
389
|
+
* database. Used by `status()` to advertise the entities a peer can
|
|
390
|
+
* talk to without per-entity bookkeeping.
|
|
391
|
+
*/
|
|
392
|
+
_listProvisionedEntities() {
|
|
393
|
+
const res = this.executor.query(`SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ? || '\\_%\\_data' ESCAPE '\\'`, [this.tablePrefix]);
|
|
394
|
+
const out = [];
|
|
395
|
+
for (const row of res.rows ?? []) {
|
|
396
|
+
const name = row.name;
|
|
397
|
+
const suffix = "_data";
|
|
398
|
+
const prefixWithU = `${this.tablePrefix}_`;
|
|
399
|
+
if (!name.startsWith(prefixWithU) || !name.endsWith(suffix))
|
|
400
|
+
continue;
|
|
401
|
+
out.push(name.slice(prefixWithU.length, name.length - suffix.length));
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ── Adapters ────────────────────────────────────────────────────────
|
|
407
|
+
/**
|
|
408
|
+
* Collect any `ReadableStream` values on `BLOB` columns into
|
|
409
|
+
* `Uint8Array` and return a shallow-copied record. Runs before the
|
|
410
|
+
* write transaction so stream failures can't leave a half-applied
|
|
411
|
+
* commit. Non-stream values pass through verbatim.
|
|
412
|
+
*/
|
|
413
|
+
async function normaliseBytesFields(record, bytesColumns) {
|
|
414
|
+
if (bytesColumns.size === 0)
|
|
415
|
+
return record;
|
|
416
|
+
const out = { ...record };
|
|
417
|
+
for (const name of bytesColumns) {
|
|
418
|
+
const v = out[name];
|
|
419
|
+
if (v === undefined || v === null)
|
|
420
|
+
continue;
|
|
421
|
+
if (v instanceof Uint8Array)
|
|
422
|
+
continue;
|
|
423
|
+
if (v instanceof ReadableStream) {
|
|
424
|
+
out[name] = await toBytes(v);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
throw new Error(`${STORE_NAME}: field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
/** Coerce a record field value into something the SQLite driver will accept. */
|
|
432
|
+
function adaptForWrite(col, value) {
|
|
433
|
+
if (value === undefined || value === null)
|
|
434
|
+
return null;
|
|
435
|
+
if (col.tag === TYPE_TAGS.JSON) {
|
|
436
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
437
|
+
}
|
|
438
|
+
if (col.tag === TYPE_TAGS.TIMESTAMP) {
|
|
439
|
+
if (value instanceof Date)
|
|
440
|
+
return value.toISOString();
|
|
441
|
+
if (typeof value === "number")
|
|
442
|
+
return new Date(value).toISOString();
|
|
443
|
+
return String(value);
|
|
444
|
+
}
|
|
445
|
+
if (col.tag === TYPE_TAGS.BOOLEAN) {
|
|
446
|
+
return value ? 1 : 0;
|
|
447
|
+
}
|
|
448
|
+
if (col.tag === TYPE_TAGS.BIGINT) {
|
|
449
|
+
// The injected sqlite executor may pass `bigint` through (e.g.
|
|
450
|
+
// `@db/sqlite`) or require `number` (e.g. the structurally-typed
|
|
451
|
+
// mock). Hand the value over unchanged for compatible drivers;
|
|
452
|
+
// numbers within safe range pass too.
|
|
453
|
+
return value;
|
|
454
|
+
}
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
/** Reconstruct an EntityRecord from a SQLite row. */
|
|
458
|
+
function adaptRowForRead(meta, row) {
|
|
459
|
+
const rec = {};
|
|
460
|
+
for (const col of meta.columns) {
|
|
461
|
+
const v = row[col.name];
|
|
462
|
+
if (v === null || v === undefined) {
|
|
463
|
+
rec[col.name] = undefined;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (col.tag === TYPE_TAGS.BYTES) {
|
|
467
|
+
rec[col.name] = rowToBytes(v);
|
|
468
|
+
}
|
|
469
|
+
else if (col.tag === TYPE_TAGS.JSON) {
|
|
470
|
+
rec[col.name] = typeof v === "string" ? JSON.parse(v) : v;
|
|
471
|
+
}
|
|
472
|
+
else if (col.tag === TYPE_TAGS.TIMESTAMP) {
|
|
473
|
+
rec[col.name] = v instanceof Date ? v : new Date(v);
|
|
474
|
+
}
|
|
475
|
+
else if (col.tag === TYPE_TAGS.BOOLEAN) {
|
|
476
|
+
rec[col.name] = Boolean(v);
|
|
477
|
+
}
|
|
478
|
+
else if (col.tag === TYPE_TAGS.BIGINT) {
|
|
479
|
+
// SQLite returns INTEGER as either JS `number` or `bigint`
|
|
480
|
+
// depending on the driver. Normalise to `bigint` so callers
|
|
481
|
+
// get a consistent shape regardless of the underlying driver.
|
|
482
|
+
rec[col.name] = typeof v === "bigint" ? v : BigInt(v);
|
|
483
|
+
}
|
|
484
|
+
else
|
|
485
|
+
rec[col.name] = v;
|
|
486
|
+
}
|
|
487
|
+
return rec;
|
|
184
488
|
}
|
package/esm/types.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Cross-cutting types (`Output`, `ProtocolInterfaceNode`,
|
|
19
19
|
* `DeleteResult`, `StatusResult`) still come from b3nd-core.
|
|
20
20
|
*/
|
|
21
|
-
import type { B3ndError, DeleteResult, Output, StatusResult } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
21
|
+
import type { B3ndError, DeleteResult, Output, StatusResult } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
22
22
|
/**
|
|
23
23
|
* Payload type accepted by `write` and produced by `read`.
|
|
24
24
|
*
|