@bandeira-tech/b3nd-save 0.8.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/LICENSE +21 -0
- package/esm/_dnt.shims.d.ts +2 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +57 -0
- package/esm/byte-entity-shim.d.ts +47 -0
- package/esm/byte-entity-shim.d.ts.map +1 -0
- package/esm/byte-entity-shim.js +138 -0
- package/esm/clients/mod.d.ts +15 -0
- package/esm/clients/mod.d.ts.map +1 -0
- package/esm/clients/mod.js +14 -0
- package/esm/clients/save-client.d.ts +87 -0
- package/esm/clients/save-client.d.ts.map +1 -0
- package/esm/clients/save-client.js +174 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts +40 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.js +31 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/client-console/client.d.ts +30 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/client-console/client.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/client-console/client.js +70 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encoding/encoding.d.ts +16 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encoding/encoding.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encoding/encoding.js +50 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts +235 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.js +619 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.d.ts +43 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/functional-client/functional-client.js +54 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/match-pattern/match-pattern.d.ts +47 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/match-pattern/match-pattern.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/match-pattern/match-pattern.js +134 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/decorators.d.ts +20 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/decorators.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/decorators.js +34 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/network.d.ts +46 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/network.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/network.js +131 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/peer.d.ts +27 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/peer.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/peer.js +25 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/flood.d.ts +59 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/flood.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/flood.js +188 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts +35 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.js +57 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/tell-and-read.d.ts +117 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/tell-and-read.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/tell-and-read.js +125 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/types.d.ts +112 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/types.js +21 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/observe-emitter/observe-emitter.d.ts +35 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/observe-emitter/observe-emitter.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/observe-emitter/observe-emitter.js +106 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts +88 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.js +86 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/events.d.ts +83 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/events.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/events.js +115 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/hooks.d.ts +164 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/hooks.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/hooks.js +67 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/identity.d.ts +99 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/identity.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/identity.js +190 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/operation-handle.d.ts +185 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/operation-handle.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/operation-handle.js +103 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/reactions.d.ts +58 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/reactions.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/reactions.js +64 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts +295 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.js +898 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts +167 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.js +5 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts +278 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.js +89 -0
- package/esm/dispatch.d.ts +35 -0
- package/esm/dispatch.d.ts.map +1 -0
- package/esm/dispatch.js +40 -0
- package/esm/elasticsearch/mod.d.ts +31 -0
- package/esm/elasticsearch/mod.d.ts.map +1 -0
- package/esm/elasticsearch/mod.js +8 -0
- package/esm/elasticsearch/store.d.ts +44 -0
- package/esm/elasticsearch/store.d.ts.map +1 -0
- package/esm/elasticsearch/store.js +213 -0
- package/esm/entity-store.d.ts +106 -0
- package/esm/entity-store.d.ts.map +1 -0
- package/esm/entity-store.js +46 -0
- package/esm/entity.d.ts +125 -0
- package/esm/entity.d.ts.map +1 -0
- package/esm/entity.js +66 -0
- package/esm/errors.d.ts +23 -0
- package/esm/errors.d.ts.map +1 -0
- package/esm/errors.js +25 -0
- package/esm/fs/mod.d.ts +28 -0
- package/esm/fs/mod.d.ts.map +1 -0
- package/esm/fs/mod.js +13 -0
- package/esm/fs/store.d.ts +45 -0
- package/esm/fs/store.d.ts.map +1 -0
- package/esm/fs/store.js +194 -0
- package/esm/indexeddb/mod.d.ts +9 -0
- package/esm/indexeddb/mod.d.ts.map +1 -0
- package/esm/indexeddb/mod.js +8 -0
- package/esm/indexeddb/store.d.ts +47 -0
- package/esm/indexeddb/store.d.ts.map +1 -0
- package/esm/indexeddb/store.js +299 -0
- package/esm/ipfs/mod.d.ts +24 -0
- package/esm/ipfs/mod.d.ts.map +1 -0
- package/esm/ipfs/mod.js +13 -0
- package/esm/ipfs/store.d.ts +42 -0
- package/esm/ipfs/store.d.ts.map +1 -0
- package/esm/ipfs/store.js +197 -0
- package/esm/localstorage/mod.d.ts +9 -0
- package/esm/localstorage/mod.d.ts.map +1 -0
- package/esm/localstorage/mod.js +8 -0
- package/esm/localstorage/store.d.ts +45 -0
- package/esm/localstorage/store.d.ts.map +1 -0
- package/esm/localstorage/store.js +147 -0
- package/esm/memory/mod.d.ts +12 -0
- package/esm/memory/mod.d.ts.map +1 -0
- package/esm/memory/mod.js +11 -0
- package/esm/memory/store.d.ts +73 -0
- package/esm/memory/store.d.ts.map +1 -0
- package/esm/memory/store.js +300 -0
- package/esm/mod.d.ts +36 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +31 -0
- package/esm/mongo/mod.d.ts +35 -0
- package/esm/mongo/mod.d.ts.map +1 -0
- package/esm/mongo/mod.js +8 -0
- package/esm/mongo/store.d.ts +42 -0
- package/esm/mongo/store.d.ts.map +1 -0
- package/esm/mongo/store.js +185 -0
- package/esm/package.json +3 -0
- package/esm/payload.d.ts +24 -0
- package/esm/payload.d.ts.map +1 -0
- package/esm/payload.js +30 -0
- package/esm/postgres/columns.d.ts +33 -0
- package/esm/postgres/columns.d.ts.map +1 -0
- package/esm/postgres/columns.js +51 -0
- package/esm/postgres/mod.d.ts +19 -0
- package/esm/postgres/mod.d.ts.map +1 -0
- package/esm/postgres/mod.js +9 -0
- package/esm/postgres/store.d.ts +75 -0
- package/esm/postgres/store.d.ts.map +1 -0
- package/esm/postgres/store.js +364 -0
- package/esm/read.d.ts +40 -0
- package/esm/read.d.ts.map +1 -0
- package/esm/read.js +67 -0
- package/esm/s3/mod.d.ts +23 -0
- package/esm/s3/mod.d.ts.map +1 -0
- package/esm/s3/mod.js +13 -0
- package/esm/s3/store.d.ts +46 -0
- package/esm/s3/store.d.ts.map +1 -0
- package/esm/s3/store.js +183 -0
- package/esm/sqlite/mod.d.ts +18 -0
- package/esm/sqlite/mod.d.ts.map +1 -0
- package/esm/sqlite/mod.js +8 -0
- package/esm/sqlite/schema.d.ts +10 -0
- package/esm/sqlite/schema.d.ts.map +1 -0
- package/esm/sqlite/schema.js +26 -0
- package/esm/sqlite/store.d.ts +42 -0
- package/esm/sqlite/store.d.ts.map +1 -0
- package/esm/sqlite/store.js +184 -0
- package/esm/types.d.ts +101 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/types.js +21 -0
- package/esm/url.d.ts +108 -0
- package/esm/url.d.ts.map +1 -0
- package/esm/url.js +151 -0
- package/package.json +93 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryStore — in-memory reference implementation of `EntityStore`.
|
|
3
|
+
*
|
|
4
|
+
* One flat `Map<uri, EntityRecord>` per provisioned entity, keyed by
|
|
5
|
+
* the entity's name. `BYTES_ENTITY` is just another entity here — its
|
|
6
|
+
* records sit in the same map as any custom entity.
|
|
7
|
+
*
|
|
8
|
+
* ## Lifecycle
|
|
9
|
+
*
|
|
10
|
+
* `entitySupport(schema)` is pure: it walks the schema's fields,
|
|
11
|
+
* picks the first recognised `TYPE_TAGS` entry per field as the
|
|
12
|
+
* canonical tag, and returns the operational handle (declared field
|
|
13
|
+
* set, bytes-field set, signature for collision detection, support
|
|
14
|
+
* report). `provisionEntity(meta)` allocates the bucket and stores
|
|
15
|
+
* the meta's signature so future collisions can be detected.
|
|
16
|
+
* `entityStatus(meta)` returns `"live"` only when the cached
|
|
17
|
+
* signature matches `meta` exactly — a same-name-different-shape
|
|
18
|
+
* schema reports `"unprovisioned"`, where "shape" includes the
|
|
19
|
+
* canonical type tag of each field (so changing `["string"]` to
|
|
20
|
+
* `["bigint"]` on an existing field name is a collision, mirroring
|
|
21
|
+
* what SQL backends catch via column-type introspection).
|
|
22
|
+
*
|
|
23
|
+
* Writes/reads/deletes consume the meta directly: there is no
|
|
24
|
+
* per-call cache lookup gate. If `meta`'s bucket does not exist on
|
|
25
|
+
* this store, `write` and `delete` surface the medium's natural
|
|
26
|
+
* "not provisioned" storage failure per entry; `read` returns misses.
|
|
27
|
+
*
|
|
28
|
+
* ## Validation
|
|
29
|
+
*
|
|
30
|
+
* A record under `meta` may only contain keys declared in
|
|
31
|
+
* `meta.declared`. Extra keys produce a per-entry
|
|
32
|
+
* `StoreWriteResult` failure. The store does not coerce.
|
|
33
|
+
*
|
|
34
|
+
* `payload: ReadableStream` (and any other field whose type tag is
|
|
35
|
+
* `"bytes"`) is collected to `Uint8Array` via `toBytes` before the
|
|
36
|
+
* record lands in the bucket. That coercion runs for every entity
|
|
37
|
+
* that declares a bytes field; nothing about it is bytes-entity-
|
|
38
|
+
* specific.
|
|
39
|
+
*/
|
|
40
|
+
import { parseUrl } from "../url.js";
|
|
41
|
+
import { storageFailure } from "../errors.js";
|
|
42
|
+
import { toBytes } from "../payload.js";
|
|
43
|
+
import { TYPE_TAGS, } from "../entity.js";
|
|
44
|
+
const STORE_NAME = "MemoryStore";
|
|
45
|
+
const KNOWN_TAGS = new Set(Object.values(TYPE_TAGS));
|
|
46
|
+
export class MemoryStore {
|
|
47
|
+
buckets = new Map();
|
|
48
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
49
|
+
entitySupport(schema) {
|
|
50
|
+
const supported = [];
|
|
51
|
+
const unsupported = [];
|
|
52
|
+
const bytesFields = new Set();
|
|
53
|
+
// Per-field canonical tag — the first recognised TYPE_TAGS entry in
|
|
54
|
+
// declaration order, matching how SQL backends pick a column type.
|
|
55
|
+
// Two schemas that resolve to different canonical tags for the same
|
|
56
|
+
// field name carry incompatible expectations and must collide.
|
|
57
|
+
const canonical = new Map();
|
|
58
|
+
for (const field of schema.fields) {
|
|
59
|
+
const first = field.type.find((t) => KNOWN_TAGS.has(t));
|
|
60
|
+
if (first === undefined) {
|
|
61
|
+
unsupported.push({
|
|
62
|
+
name: field.name,
|
|
63
|
+
reason: field.type.length === 0
|
|
64
|
+
? "field declares no type tags"
|
|
65
|
+
: `no recognised tag in [${field.type.join(", ")}]`,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
supported.push(field.name);
|
|
70
|
+
canonical.set(field.name, first);
|
|
71
|
+
if (first === TYPE_TAGS.BYTES)
|
|
72
|
+
bytesFields.add(field.name);
|
|
73
|
+
}
|
|
74
|
+
const support = {
|
|
75
|
+
entity: schema.name,
|
|
76
|
+
supported,
|
|
77
|
+
unsupported,
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
support,
|
|
81
|
+
declared: new Set(supported),
|
|
82
|
+
bytesFields,
|
|
83
|
+
signature: computeSignature(schema.name, canonical),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// deno-lint-ignore require-await
|
|
87
|
+
async entityStatus(meta) {
|
|
88
|
+
const bucket = this.buckets.get(meta.support.entity);
|
|
89
|
+
if (!bucket)
|
|
90
|
+
return "unprovisioned";
|
|
91
|
+
return bucket.signature === meta.signature ? "live" : "unprovisioned";
|
|
92
|
+
}
|
|
93
|
+
// deno-lint-ignore require-await
|
|
94
|
+
async provisionEntity(meta) {
|
|
95
|
+
const existing = this.buckets.get(meta.support.entity);
|
|
96
|
+
if (existing) {
|
|
97
|
+
if (existing.signature !== meta.signature) {
|
|
98
|
+
throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
|
|
99
|
+
`provisioned with a different shape`);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.buckets.set(meta.support.entity, {
|
|
104
|
+
signature: meta.signature,
|
|
105
|
+
records: new Map(),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// ── Write ────────────────────────────────────────────────────────
|
|
109
|
+
async write(meta, entries) {
|
|
110
|
+
const bucket = this.buckets.get(meta.support.entity);
|
|
111
|
+
const results = [];
|
|
112
|
+
for (const { uri, record } of entries) {
|
|
113
|
+
if (!bucket) {
|
|
114
|
+
results.push({
|
|
115
|
+
success: false,
|
|
116
|
+
...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
|
|
121
|
+
if (extras.length > 0) {
|
|
122
|
+
results.push({
|
|
123
|
+
success: false,
|
|
124
|
+
...storageFailure(new Error(`record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const normalised = needsBytesNormalisation(record, meta.bytesFields)
|
|
130
|
+
? await normaliseBytesFields(record, meta.bytesFields)
|
|
131
|
+
: { ...record };
|
|
132
|
+
bucket.records.set(uri, normalised);
|
|
133
|
+
results.push({ success: true });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
results.push({
|
|
137
|
+
success: false,
|
|
138
|
+
...storageFailure(err, "Write failed", uri),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
// ── Read ─────────────────────────────────────────────────────────
|
|
145
|
+
// deno-lint-ignore require-await
|
|
146
|
+
async read(meta, urls) {
|
|
147
|
+
const bucket = this.buckets.get(meta.support.entity);
|
|
148
|
+
return urls.map((url) => {
|
|
149
|
+
const parsed = parseUrl(url);
|
|
150
|
+
switch (parsed.fn) {
|
|
151
|
+
case "read":
|
|
152
|
+
return [url, bucket?.records.get(parsed.uri)];
|
|
153
|
+
case "ls":
|
|
154
|
+
return [url, this._list(bucket?.records, parsed)];
|
|
155
|
+
case "count":
|
|
156
|
+
return [url, this._count(bucket?.records, parsed)];
|
|
157
|
+
default:
|
|
158
|
+
throw new Error(`${STORE_NAME}: unsupported fn '${parsed.fn}'`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
_walk(bucket, uri) {
|
|
163
|
+
if (!bucket)
|
|
164
|
+
return [];
|
|
165
|
+
const prefix = uri.endsWith("/") ? uri : `${uri}/`;
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const [k, rec] of bucket) {
|
|
168
|
+
if (!k.startsWith(prefix))
|
|
169
|
+
continue;
|
|
170
|
+
const rest = k.slice(prefix.length);
|
|
171
|
+
if (rest.length === 0 || rest.includes("/"))
|
|
172
|
+
continue;
|
|
173
|
+
out.push([k, rec]);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
_list(bucket, parsed) {
|
|
178
|
+
const { params } = parsed;
|
|
179
|
+
if (params.pattern !== undefined) {
|
|
180
|
+
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
181
|
+
}
|
|
182
|
+
if (params.sortBy !== undefined && params.sortBy !== "uri") {
|
|
183
|
+
throw new Error(`${STORE_NAME}: unsupported sortBy: ${params.sortBy}`);
|
|
184
|
+
}
|
|
185
|
+
const format = params.format ?? "full";
|
|
186
|
+
if (format !== "full" && format !== "uris") {
|
|
187
|
+
throw new Error(`${STORE_NAME}: unsupported format: ${format}`);
|
|
188
|
+
}
|
|
189
|
+
let entries = this._walk(bucket, parsed.uri);
|
|
190
|
+
if (params.sortBy === "uri") {
|
|
191
|
+
const dir = params.sortOrder === "desc" ? -1 : 1;
|
|
192
|
+
entries = [...entries].sort(([a], [b]) => a.localeCompare(b) * dir);
|
|
193
|
+
}
|
|
194
|
+
if (params.limit !== undefined) {
|
|
195
|
+
const page = params.page ?? 1;
|
|
196
|
+
const start = (page - 1) * params.limit;
|
|
197
|
+
entries = entries.slice(start, start + params.limit);
|
|
198
|
+
}
|
|
199
|
+
if (format === "uris")
|
|
200
|
+
return entries.map(([uri]) => uri);
|
|
201
|
+
return entries;
|
|
202
|
+
}
|
|
203
|
+
_count(bucket, parsed) {
|
|
204
|
+
if (parsed.params.pattern !== undefined) {
|
|
205
|
+
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
206
|
+
}
|
|
207
|
+
return this._walk(bucket, parsed.uri).length;
|
|
208
|
+
}
|
|
209
|
+
// ── Delete ───────────────────────────────────────────────────────
|
|
210
|
+
delete(meta, uris) {
|
|
211
|
+
const bucket = this.buckets.get(meta.support.entity);
|
|
212
|
+
const results = [];
|
|
213
|
+
for (const uri of uris) {
|
|
214
|
+
if (!bucket) {
|
|
215
|
+
results.push({
|
|
216
|
+
success: false,
|
|
217
|
+
...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
bucket.records.delete(uri);
|
|
223
|
+
results.push({ success: true });
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
results.push({
|
|
227
|
+
success: false,
|
|
228
|
+
...storageFailure(err, "Delete failed", uri),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return Promise.resolve(results);
|
|
233
|
+
}
|
|
234
|
+
// ── Status / capabilities ────────────────────────────────────────
|
|
235
|
+
status() {
|
|
236
|
+
return Promise.resolve({
|
|
237
|
+
status: "healthy",
|
|
238
|
+
schema: [...this.buckets.keys()].map((n) => `entity:${n}`),
|
|
239
|
+
fns: ["read", "ls", "count"],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
capabilities() {
|
|
243
|
+
return { atomicBatch: false };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Canonical signature for collision detection. Two metas produced
|
|
248
|
+
* from semantically-identical schemas (same name, same set of
|
|
249
|
+
* supported fields, same canonical type tag per field) yield the same
|
|
250
|
+
* string; any shape difference — including a field whose tag changed
|
|
251
|
+
* from `"string"` to `"bigint"` — yields a different one.
|
|
252
|
+
*
|
|
253
|
+
* Matches the granularity SQL backends get for free via column-type
|
|
254
|
+
* introspection: a tag swap on an existing field is a collision even
|
|
255
|
+
* though the field name is unchanged.
|
|
256
|
+
*/
|
|
257
|
+
function computeSignature(name, canonical) {
|
|
258
|
+
const fields = [...canonical.entries()]
|
|
259
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
260
|
+
return JSON.stringify({ name, fields });
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Quick predicate: does the record have a `ReadableStream` (or a
|
|
264
|
+
* non-bytes value that needs to be rejected) on any of its bytes
|
|
265
|
+
* fields? Lets the write hot path skip the async normalisation step
|
|
266
|
+
* when every value is already a `Uint8Array`.
|
|
267
|
+
*/
|
|
268
|
+
function needsBytesNormalisation(record, bytesFields) {
|
|
269
|
+
for (const name of bytesFields) {
|
|
270
|
+
const v = record[name];
|
|
271
|
+
if (v === undefined || v === null)
|
|
272
|
+
continue;
|
|
273
|
+
if (v instanceof Uint8Array)
|
|
274
|
+
continue;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Collect any `ReadableStream` values on `bytes`-tagged fields into
|
|
281
|
+
* `Uint8Array` and return a shallow-copied record. Non-stream values
|
|
282
|
+
* pass through; this is the same coercion every backend with a
|
|
283
|
+
* non-streaming write path performs, generalised across fields.
|
|
284
|
+
*/
|
|
285
|
+
async function normaliseBytesFields(record, bytesFields) {
|
|
286
|
+
const out = { ...record };
|
|
287
|
+
for (const name of bytesFields) {
|
|
288
|
+
const v = out[name];
|
|
289
|
+
if (v === undefined || v === null)
|
|
290
|
+
continue;
|
|
291
|
+
if (v instanceof Uint8Array)
|
|
292
|
+
continue;
|
|
293
|
+
if (v instanceof ReadableStream) {
|
|
294
|
+
out[name] = await toBytes(v);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
throw new Error(`field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
package/esm/mod.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* b3nd-save — the data-saving layer for b3nd.
|
|
4
|
+
*
|
|
5
|
+
* This root barrel re-exports every public subpath as a namespace.
|
|
6
|
+
* Use it for convenience or discoverability; for the smallest
|
|
7
|
+
* footprint, import the subpaths directly (e.g.
|
|
8
|
+
* `@bandeira-tech/b3nd-save/postgres`).
|
|
9
|
+
*
|
|
10
|
+
* Namespaces:
|
|
11
|
+
* clients — EntityStore → ProtocolInterfaceNode adapters
|
|
12
|
+
* memory — In-memory reference backend
|
|
13
|
+
* postgres, mongo, sqlite, fs, ipfs, s3, elasticsearch,
|
|
14
|
+
* localstorage, indexeddb — EntityStore backend implementations
|
|
15
|
+
*
|
|
16
|
+
* The entity surface (`EntityStore`, `EntitySchema`, `BYTES_ENTITY`,
|
|
17
|
+
* `TYPE_TAGS`, …) is re-exported from the root for convenience.
|
|
18
|
+
*/
|
|
19
|
+
export type { StoreCapabilities, StoreWriteResult } from "./types.js";
|
|
20
|
+
export type { ParsedUrl, ReadParams } from "./url.js";
|
|
21
|
+
export { buildUrl, parseUrl, uriOf } from "./url.js";
|
|
22
|
+
export type { EntityField, EntityMeta, EntityRecord, EntitySchema, EntitySupport, } from "./entity.js";
|
|
23
|
+
export { BYTES_ENTITY, TYPE_TAGS } from "./entity.js";
|
|
24
|
+
export type { EntityStore } from "./entity-store.js";
|
|
25
|
+
export * as clients from "./clients/mod.js";
|
|
26
|
+
export * as memory from "./memory/mod.js";
|
|
27
|
+
export * as postgres from "./postgres/mod.js";
|
|
28
|
+
export * as mongo from "./mongo/mod.js";
|
|
29
|
+
export * as sqlite from "./sqlite/mod.js";
|
|
30
|
+
export * as fs from "./fs/mod.js";
|
|
31
|
+
export * as ipfs from "./ipfs/mod.js";
|
|
32
|
+
export * as s3 from "./s3/mod.js";
|
|
33
|
+
export * as elasticsearch from "./elasticsearch/mod.js";
|
|
34
|
+
export * as localstorage from "./localstorage/mod.js";
|
|
35
|
+
export * as indexeddb from "./indexeddb/mod.js";
|
|
36
|
+
//# sourceMappingURL=mod.d.ts.map
|
package/esm/mod.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAErD,YAAY,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,YAAY,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAC"}
|
package/esm/mod.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* b3nd-save — the data-saving layer for b3nd.
|
|
4
|
+
*
|
|
5
|
+
* This root barrel re-exports every public subpath as a namespace.
|
|
6
|
+
* Use it for convenience or discoverability; for the smallest
|
|
7
|
+
* footprint, import the subpaths directly (e.g.
|
|
8
|
+
* `@bandeira-tech/b3nd-save/postgres`).
|
|
9
|
+
*
|
|
10
|
+
* Namespaces:
|
|
11
|
+
* clients — EntityStore → ProtocolInterfaceNode adapters
|
|
12
|
+
* memory — In-memory reference backend
|
|
13
|
+
* postgres, mongo, sqlite, fs, ipfs, s3, elasticsearch,
|
|
14
|
+
* localstorage, indexeddb — EntityStore backend implementations
|
|
15
|
+
*
|
|
16
|
+
* The entity surface (`EntityStore`, `EntitySchema`, `BYTES_ENTITY`,
|
|
17
|
+
* `TYPE_TAGS`, …) is re-exported from the root for convenience.
|
|
18
|
+
*/
|
|
19
|
+
export { buildUrl, parseUrl, uriOf } from "./url.js";
|
|
20
|
+
export { BYTES_ENTITY, TYPE_TAGS } from "./entity.js";
|
|
21
|
+
export * as clients from "./clients/mod.js";
|
|
22
|
+
export * as memory from "./memory/mod.js";
|
|
23
|
+
export * as postgres from "./postgres/mod.js";
|
|
24
|
+
export * as mongo from "./mongo/mod.js";
|
|
25
|
+
export * as sqlite from "./sqlite/mod.js";
|
|
26
|
+
export * as fs from "./fs/mod.js";
|
|
27
|
+
export * as ipfs from "./ipfs/mod.js";
|
|
28
|
+
export * as s3 from "./s3/mod.js";
|
|
29
|
+
export * as elasticsearch from "./elasticsearch/mod.js";
|
|
30
|
+
export * as localstorage from "./localstorage/mod.js";
|
|
31
|
+
export * as indexeddb from "./indexeddb/mod.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoDB backend for b3nd.
|
|
3
|
+
*
|
|
4
|
+
* Store implementation backed by MongoDB. Requires an injected
|
|
5
|
+
* MongoExecutor so the package does not depend on a specific MongoDB
|
|
6
|
+
* driver.
|
|
7
|
+
*/
|
|
8
|
+
export interface MongoFindManyOptions {
|
|
9
|
+
sort?: Record<string, 1 | -1>;
|
|
10
|
+
skip?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
/** Mongo projection map (1 = include, 0 = exclude). */
|
|
13
|
+
projection?: Record<string, 0 | 1>;
|
|
14
|
+
}
|
|
15
|
+
export interface MongoExecutor {
|
|
16
|
+
insertOne(doc: Record<string, unknown>): Promise<{
|
|
17
|
+
acknowledged?: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
updateOne(filter: Record<string, unknown>, update: Record<string, unknown>, options?: Record<string, unknown>): Promise<{
|
|
20
|
+
matchedCount?: number;
|
|
21
|
+
modifiedCount?: number;
|
|
22
|
+
upsertedId?: unknown;
|
|
23
|
+
}>;
|
|
24
|
+
findOne(filter: Record<string, unknown>): Promise<Record<string, unknown> | null>;
|
|
25
|
+
findMany(filter: Record<string, unknown>, options?: MongoFindManyOptions): Promise<Record<string, unknown>[]>;
|
|
26
|
+
countDocuments(filter: Record<string, unknown>): Promise<number>;
|
|
27
|
+
deleteOne(filter: Record<string, unknown>): Promise<{
|
|
28
|
+
deletedCount?: number;
|
|
29
|
+
}>;
|
|
30
|
+
ping(): Promise<boolean>;
|
|
31
|
+
transaction?: <T>(fn: (executor: MongoExecutor) => Promise<T>) => Promise<T>;
|
|
32
|
+
cleanup?: () => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export { MongoStore } from "./store.js";
|
|
35
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/mongo/mod.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC7E,SAAS,CACP,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC,OAAO,CACR;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CACxE,CAAC;IACF,OAAO,CACL,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3C,QAAQ,CACN,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IACtC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjE,SAAS,CACP,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
package/esm/mongo/mod.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoStore — MongoDB implementation of Store.
|
|
3
|
+
*
|
|
4
|
+
* Pure mechanical byte storage with no protocol awareness. Uses an
|
|
5
|
+
* injected MongoExecutor so the package does not depend on a specific
|
|
6
|
+
* MongoDB driver. Payload is stored as a BSON `Binary` value (the
|
|
7
|
+
* `mongodb` driver maps `Uint8Array` to it transparently).
|
|
8
|
+
*
|
|
9
|
+
* `fn=ls`/`fn=count` push down to a regex prefix query that enforces
|
|
10
|
+
* the shallow-direct-leaves contract: `^<prefix>[^/]+$`.
|
|
11
|
+
*/
|
|
12
|
+
import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.js";
|
|
13
|
+
import { type BytesOnlyEntityMeta } from "../byte-entity-shim.js";
|
|
14
|
+
import type { EntityStore } from "../entity-store.js";
|
|
15
|
+
import type { EntityRecord, EntitySchema } from "../entity.js";
|
|
16
|
+
import type { StoreCapabilities, StoreWriteResult } from "../types.js";
|
|
17
|
+
import type { MongoExecutor } from "./mod.js";
|
|
18
|
+
export declare class MongoStore implements EntityStore<BytesOnlyEntityMeta> {
|
|
19
|
+
private readonly collectionName;
|
|
20
|
+
private readonly executor;
|
|
21
|
+
constructor(collectionName: string, executor: MongoExecutor);
|
|
22
|
+
entitySupport(schema: EntitySchema): BytesOnlyEntityMeta;
|
|
23
|
+
entityStatus(meta: BytesOnlyEntityMeta): Promise<"live" | "unprovisioned">;
|
|
24
|
+
provisionEntity(_meta: BytesOnlyEntityMeta): Promise<void>;
|
|
25
|
+
write(meta: BytesOnlyEntityMeta, entries: {
|
|
26
|
+
uri: string;
|
|
27
|
+
record: EntityRecord;
|
|
28
|
+
}[]): Promise<StoreWriteResult[]>;
|
|
29
|
+
read<T = EntityRecord | undefined>(meta: BytesOnlyEntityMeta, urls: string[]): Promise<Output<T>[]>;
|
|
30
|
+
delete(meta: BytesOnlyEntityMeta, uris: string[]): Promise<DeleteResult[]>;
|
|
31
|
+
private _writeBytes;
|
|
32
|
+
private _readBytes;
|
|
33
|
+
private _readOne;
|
|
34
|
+
/** Build the shallow-direct-leaves regex filter for a prefix. */
|
|
35
|
+
private _leafFilter;
|
|
36
|
+
private _ls;
|
|
37
|
+
private _count;
|
|
38
|
+
private _deleteBytes;
|
|
39
|
+
status(): Promise<StatusResult>;
|
|
40
|
+
capabilities(): StoreCapabilities;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/mongo/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAC3E,OAAO,EAEL,KAAK,mBAAmB,EAIzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAO/D,OAAO,KAAK,EACV,iBAAiB,EAEjB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiC9C,qBAAa,UAAW,YAAW,WAAW,CAAC,mBAAmB,CAAC;IACjE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;gBAE7B,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa;IAU3D,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,mBAAmB;IAIxD,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IAI1E,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,KAAK,CACH,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAS9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IASvB,MAAM,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;YAW5D,WAAW;IA+BzB,OAAO,CAAC,UAAU;YAQJ,QAAQ;IAMtB,iEAAiE;IACjE,OAAO,CAAC,WAAW;YAIL,GAAG;YA2BH,MAAM;YAON,YAAY;IAoBpB,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAyBrC,YAAY,IAAI,iBAAiB;CAGlC"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MongoStore — MongoDB implementation of Store.
|
|
3
|
+
*
|
|
4
|
+
* Pure mechanical byte storage with no protocol awareness. Uses an
|
|
5
|
+
* injected MongoExecutor so the package does not depend on a specific
|
|
6
|
+
* MongoDB driver. Payload is stored as a BSON `Binary` value (the
|
|
7
|
+
* `mongodb` driver maps `Uint8Array` to it transparently).
|
|
8
|
+
*
|
|
9
|
+
* `fn=ls`/`fn=count` push down to a regex prefix query that enforces
|
|
10
|
+
* the shallow-direct-leaves contract: `^<prefix>[^/]+$`.
|
|
11
|
+
*/
|
|
12
|
+
import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
|
|
13
|
+
import { dispatchRead } from "../dispatch.js";
|
|
14
|
+
import { storageFailure } from "../errors.js";
|
|
15
|
+
import { toBytes } from "../payload.js";
|
|
16
|
+
import { validateReadParams } from "../read.js";
|
|
17
|
+
const STORE_NAME = "MongoStore";
|
|
18
|
+
/** Escape special regex characters for safe use in a RegExp pattern. */
|
|
19
|
+
function escapeRegex(input) {
|
|
20
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a Mongo document field into a `Uint8Array`. The driver may
|
|
24
|
+
* surface BSON `Binary` (which exposes `.buffer`) or a raw
|
|
25
|
+
* `Uint8Array` depending on options.
|
|
26
|
+
*/
|
|
27
|
+
function docToBytes(value) {
|
|
28
|
+
if (value instanceof Uint8Array)
|
|
29
|
+
return value;
|
|
30
|
+
if (value && typeof value === "object" &&
|
|
31
|
+
"buffer" in value &&
|
|
32
|
+
value.buffer instanceof Uint8Array) {
|
|
33
|
+
return value.buffer;
|
|
34
|
+
}
|
|
35
|
+
if (value && typeof value === "object" &&
|
|
36
|
+
"buffer" in value &&
|
|
37
|
+
value.buffer instanceof ArrayBuffer) {
|
|
38
|
+
return new Uint8Array(value.buffer);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`MongoStore: unexpected payload type ${typeof value}`);
|
|
41
|
+
}
|
|
42
|
+
export class MongoStore {
|
|
43
|
+
collectionName;
|
|
44
|
+
executor;
|
|
45
|
+
constructor(collectionName, executor) {
|
|
46
|
+
if (!collectionName)
|
|
47
|
+
throw new Error("collectionName is required");
|
|
48
|
+
if (!executor)
|
|
49
|
+
throw new Error("executor is required");
|
|
50
|
+
this.collectionName = collectionName;
|
|
51
|
+
this.executor = executor;
|
|
52
|
+
}
|
|
53
|
+
// ── EntityStore surface ──────────────────────────────────────────
|
|
54
|
+
entitySupport(schema) {
|
|
55
|
+
return bytesOnlyEntitySupport(schema);
|
|
56
|
+
}
|
|
57
|
+
entityStatus(meta) {
|
|
58
|
+
return Promise.resolve(meta.isBytes ? "live" : "unprovisioned");
|
|
59
|
+
}
|
|
60
|
+
provisionEntity(_meta) {
|
|
61
|
+
return Promise.resolve();
|
|
62
|
+
}
|
|
63
|
+
write(meta, entries) {
|
|
64
|
+
return bytesOnlyWrite(meta, STORE_NAME, entries, (e) => this._writeBytes(e));
|
|
65
|
+
}
|
|
66
|
+
read(meta, urls) {
|
|
67
|
+
return bytesOnlyRead(meta, STORE_NAME, urls, (u) => this._readBytes(u));
|
|
68
|
+
}
|
|
69
|
+
delete(meta, uris) {
|
|
70
|
+
return bytesOnlyDelete(meta, STORE_NAME, uris, (u) => this._deleteBytes(u));
|
|
71
|
+
}
|
|
72
|
+
// ── Byte ops (BYTES_ENTITY routing) ──────────────────────────────
|
|
73
|
+
async _writeBytes(entries) {
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
try {
|
|
77
|
+
const bytes = await toBytes(entry.payload);
|
|
78
|
+
await this.executor.updateOne({ uri: entry.uri }, {
|
|
79
|
+
$set: {
|
|
80
|
+
uri: entry.uri,
|
|
81
|
+
payload: bytes,
|
|
82
|
+
updatedAt: new Date(),
|
|
83
|
+
},
|
|
84
|
+
}, { upsert: true });
|
|
85
|
+
results.push({ success: true });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
results.push({
|
|
89
|
+
success: false,
|
|
90
|
+
...storageFailure(err, "Write failed", entry.uri),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
_readBytes(urls) {
|
|
97
|
+
return dispatchRead(urls, STORE_NAME, {
|
|
98
|
+
read: (p) => this._readOne(p.uri),
|
|
99
|
+
ls: (p) => this._ls(p),
|
|
100
|
+
count: (p) => this._count(p),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async _readOne(uri) {
|
|
104
|
+
const doc = await this.executor.findOne({ uri });
|
|
105
|
+
if (!doc)
|
|
106
|
+
return undefined;
|
|
107
|
+
return docToBytes(doc.payload);
|
|
108
|
+
}
|
|
109
|
+
/** Build the shallow-direct-leaves regex filter for a prefix. */
|
|
110
|
+
_leafFilter(prefixUri) {
|
|
111
|
+
return { uri: { $regex: `^${escapeRegex(prefixUri)}[^/]+$` } };
|
|
112
|
+
}
|
|
113
|
+
async _ls(parsed) {
|
|
114
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
115
|
+
const { params } = parsed;
|
|
116
|
+
const format = params.format ?? "full";
|
|
117
|
+
const options = {};
|
|
118
|
+
if (params.sortBy === "uri") {
|
|
119
|
+
options.sort = { uri: params.sortOrder === "desc" ? -1 : 1 };
|
|
120
|
+
}
|
|
121
|
+
if (params.limit !== undefined) {
|
|
122
|
+
const page = params.page ?? 1;
|
|
123
|
+
options.limit = params.limit;
|
|
124
|
+
options.skip = (page - 1) * params.limit;
|
|
125
|
+
}
|
|
126
|
+
if (format === "uris") {
|
|
127
|
+
options.projection = { uri: 1, _id: 0 };
|
|
128
|
+
}
|
|
129
|
+
const docs = await this.executor.findMany(this._leafFilter(parsed.uri), options);
|
|
130
|
+
if (format === "uris")
|
|
131
|
+
return docs.map((d) => d.uri);
|
|
132
|
+
return docs.map((d) => [d.uri, docToBytes(d.payload)]);
|
|
133
|
+
}
|
|
134
|
+
async _count(parsed) {
|
|
135
|
+
if (parsed.params.pattern !== undefined) {
|
|
136
|
+
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
137
|
+
}
|
|
138
|
+
return await this.executor.countDocuments(this._leafFilter(parsed.uri));
|
|
139
|
+
}
|
|
140
|
+
async _deleteBytes(uris) {
|
|
141
|
+
const results = [];
|
|
142
|
+
for (const uri of uris) {
|
|
143
|
+
try {
|
|
144
|
+
await this.executor.deleteOne({ uri });
|
|
145
|
+
results.push({ success: true });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
results.push({
|
|
149
|
+
success: false,
|
|
150
|
+
...storageFailure(err, "Delete failed", uri),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
// ── Status ───────────────────────────────────────────────────────
|
|
157
|
+
async status() {
|
|
158
|
+
try {
|
|
159
|
+
const ok = await this.executor.ping();
|
|
160
|
+
if (!ok) {
|
|
161
|
+
return {
|
|
162
|
+
status: "unhealthy",
|
|
163
|
+
message: "MongoDB ping failed",
|
|
164
|
+
fns: ["read", "ls", "count"],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
status: "healthy",
|
|
169
|
+
message: "MongoDB store is operational",
|
|
170
|
+
fns: ["read", "ls", "count"],
|
|
171
|
+
details: { collectionName: this.collectionName },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
status: "unhealthy",
|
|
177
|
+
message: error instanceof Error ? error.message : String(error),
|
|
178
|
+
fns: ["read", "ls", "count"],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
capabilities() {
|
|
183
|
+
return { atomicBatch: false };
|
|
184
|
+
}
|
|
185
|
+
}
|
package/esm/package.json
ADDED