@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
package/esm/payload.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload normalizers for backends.
|
|
3
|
+
*
|
|
4
|
+
* `StorePayload = Uint8Array | ReadableStream<Uint8Array>`. Backends
|
|
5
|
+
* with no native streaming path call `toBytes` on write to collect
|
|
6
|
+
* incoming streams. Backends that natively stream call `toStream` on
|
|
7
|
+
* read to wrap a buffered value back into a `ReadableStream` when
|
|
8
|
+
* that's the cheap shape for the caller.
|
|
9
|
+
*
|
|
10
|
+
* Both helpers go through `Response` so we get the platform's
|
|
11
|
+
* battle-tested coercion (handles `Uint8Array`, `ReadableStream`,
|
|
12
|
+
* locked stream errors, etc.) without re-implementing.
|
|
13
|
+
*/
|
|
14
|
+
import type { StorePayload } from "./types.js";
|
|
15
|
+
/** Collect a `StorePayload` into a single `Uint8Array`. */
|
|
16
|
+
export declare function toBytes(payload: StorePayload): Promise<Uint8Array>;
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a `StorePayload` as a `ReadableStream<Uint8Array>`. A
|
|
19
|
+
* `Uint8Array` becomes a single-chunk stream; an existing stream
|
|
20
|
+
* passes through. Use this on read paths from backends that prefer to
|
|
21
|
+
* yield a stream to callers.
|
|
22
|
+
*/
|
|
23
|
+
export declare function toStream(payload: StorePayload): ReadableStream<Uint8Array>;
|
|
24
|
+
//# sourceMappingURL=payload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../src/payload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,2DAA2D;AAC3D,wBAAsB,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,CAGxE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,cAAc,CAAC,UAAU,CAAC,CAG1E"}
|
package/esm/payload.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payload normalizers for backends.
|
|
3
|
+
*
|
|
4
|
+
* `StorePayload = Uint8Array | ReadableStream<Uint8Array>`. Backends
|
|
5
|
+
* with no native streaming path call `toBytes` on write to collect
|
|
6
|
+
* incoming streams. Backends that natively stream call `toStream` on
|
|
7
|
+
* read to wrap a buffered value back into a `ReadableStream` when
|
|
8
|
+
* that's the cheap shape for the caller.
|
|
9
|
+
*
|
|
10
|
+
* Both helpers go through `Response` so we get the platform's
|
|
11
|
+
* battle-tested coercion (handles `Uint8Array`, `ReadableStream`,
|
|
12
|
+
* locked stream errors, etc.) without re-implementing.
|
|
13
|
+
*/
|
|
14
|
+
/** Collect a `StorePayload` into a single `Uint8Array`. */
|
|
15
|
+
export async function toBytes(payload) {
|
|
16
|
+
if (payload instanceof Uint8Array)
|
|
17
|
+
return payload;
|
|
18
|
+
return new Uint8Array(await new Response(payload).arrayBuffer());
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wrap a `StorePayload` as a `ReadableStream<Uint8Array>`. A
|
|
22
|
+
* `Uint8Array` becomes a single-chunk stream; an existing stream
|
|
23
|
+
* passes through. Use this on read paths from backends that prefer to
|
|
24
|
+
* yield a stream to callers.
|
|
25
|
+
*/
|
|
26
|
+
export function toStream(payload) {
|
|
27
|
+
if (payload instanceof ReadableStream)
|
|
28
|
+
return payload;
|
|
29
|
+
return new Response(payload).body;
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TYPE_TAGS → Postgres column type mapping for native entity tables.
|
|
3
|
+
*
|
|
4
|
+
* Each canonical tag in `TYPE_TAGS` resolves to one SQL type. A field
|
|
5
|
+
* carrying multiple tags (e.g. `["string", "email"]`) picks the first
|
|
6
|
+
* recognized canonical tag in declaration order — extra refinement
|
|
7
|
+
* tags pass through without affecting storage.
|
|
8
|
+
*/
|
|
9
|
+
import { type EntityField } from "../entity.js";
|
|
10
|
+
export interface ColumnPlan {
|
|
11
|
+
/** Field/column name. */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Postgres SQL type. */
|
|
14
|
+
sqlType: string;
|
|
15
|
+
/** The recognized tag that decided `sqlType` (for diagnostics). */
|
|
16
|
+
tag: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ColumnPlanResult {
|
|
19
|
+
columns: ColumnPlan[];
|
|
20
|
+
unsupported: {
|
|
21
|
+
name: string;
|
|
22
|
+
reason: string;
|
|
23
|
+
}[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a schema's fields into Postgres column plans.
|
|
27
|
+
*
|
|
28
|
+
* Skips fields whose tags are all outside `TYPE_TAGS`; reports them
|
|
29
|
+
* via `unsupported`. Rejects invalid field names up front so we never
|
|
30
|
+
* emit unsafe SQL identifiers.
|
|
31
|
+
*/
|
|
32
|
+
export declare function planColumns(fields: EntityField[]): ColumnPlanResult;
|
|
33
|
+
//# sourceMappingURL=columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../../src/postgres/columns.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,KAAK,WAAW,EAAa,MAAM,cAAc,CAAC;AAc3D,MAAM,WAAW,UAAU;IACzB,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,gBAAgB,CA0BnE"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TYPE_TAGS → Postgres column type mapping for native entity tables.
|
|
3
|
+
*
|
|
4
|
+
* Each canonical tag in `TYPE_TAGS` resolves to one SQL type. A field
|
|
5
|
+
* carrying multiple tags (e.g. `["string", "email"]`) picks the first
|
|
6
|
+
* recognized canonical tag in declaration order — extra refinement
|
|
7
|
+
* tags pass through without affecting storage.
|
|
8
|
+
*/
|
|
9
|
+
import { TYPE_TAGS } from "../entity.js";
|
|
10
|
+
const SQL_TYPE = {
|
|
11
|
+
[TYPE_TAGS.STRING]: "TEXT",
|
|
12
|
+
[TYPE_TAGS.NUMBER]: "DOUBLE PRECISION",
|
|
13
|
+
[TYPE_TAGS.BIGINT]: "BIGINT",
|
|
14
|
+
[TYPE_TAGS.BOOLEAN]: "BOOLEAN",
|
|
15
|
+
[TYPE_TAGS.BYTES]: "BYTEA",
|
|
16
|
+
[TYPE_TAGS.TIMESTAMP]: "TIMESTAMPTZ",
|
|
17
|
+
[TYPE_TAGS.JSON]: "JSONB",
|
|
18
|
+
};
|
|
19
|
+
const FIELD_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a schema's fields into Postgres column plans.
|
|
22
|
+
*
|
|
23
|
+
* Skips fields whose tags are all outside `TYPE_TAGS`; reports them
|
|
24
|
+
* via `unsupported`. Rejects invalid field names up front so we never
|
|
25
|
+
* emit unsafe SQL identifiers.
|
|
26
|
+
*/
|
|
27
|
+
export function planColumns(fields) {
|
|
28
|
+
const columns = [];
|
|
29
|
+
const unsupported = [];
|
|
30
|
+
for (const field of fields) {
|
|
31
|
+
if (!FIELD_NAME.test(field.name)) {
|
|
32
|
+
unsupported.push({
|
|
33
|
+
name: field.name,
|
|
34
|
+
reason: `field name must match ${FIELD_NAME.source}; got '${field.name}'`,
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const tag = field.type.find((t) => SQL_TYPE[t] !== undefined);
|
|
39
|
+
if (!tag) {
|
|
40
|
+
unsupported.push({
|
|
41
|
+
name: field.name,
|
|
42
|
+
reason: field.type.length === 0
|
|
43
|
+
? "field declares no type tags"
|
|
44
|
+
: `no recognised tag in [${field.type.join(", ")}]`,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
columns.push({ name: field.name, sqlType: SQL_TYPE[tag], tag });
|
|
49
|
+
}
|
|
50
|
+
return { columns, unsupported };
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL backend for b3nd.
|
|
3
|
+
*
|
|
4
|
+
* `EntityStore` implementation backed by PostgreSQL. Requires an
|
|
5
|
+
* injected `SqlExecutor` so the SDK does not depend on a specific
|
|
6
|
+
* Postgres driver. Tables are provisioned lazily via `ensureEntity` —
|
|
7
|
+
* no separate schema-bootstrap step.
|
|
8
|
+
*/
|
|
9
|
+
export interface SqlExecutorResult {
|
|
10
|
+
rows: unknown[];
|
|
11
|
+
rowCount?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SqlExecutor {
|
|
14
|
+
query: (sql: string, args?: unknown[]) => Promise<SqlExecutorResult>;
|
|
15
|
+
transaction: <T>(fn: (tx: SqlExecutor) => Promise<T>) => Promise<T>;
|
|
16
|
+
cleanup?: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export { PostgresStore } from "./store.js";
|
|
19
|
+
//# sourceMappingURL=mod.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/postgres/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACrE,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL backend for b3nd.
|
|
3
|
+
*
|
|
4
|
+
* `EntityStore` implementation backed by PostgreSQL. Requires an
|
|
5
|
+
* injected `SqlExecutor` so the SDK does not depend on a specific
|
|
6
|
+
* Postgres driver. Tables are provisioned lazily via `ensureEntity` —
|
|
7
|
+
* no separate schema-bootstrap step.
|
|
8
|
+
*/
|
|
9
|
+
export { PostgresStore } from "./store.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgresStore — PostgreSQL implementation of `EntityStore`.
|
|
3
|
+
*
|
|
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 BYTEA` column — no legacy
|
|
8
|
+
* special case.
|
|
9
|
+
*
|
|
10
|
+
* ## Lifecycle
|
|
11
|
+
*
|
|
12
|
+
* `entitySupport(schema)` is pure — it runs `planColumns` and returns
|
|
13
|
+
* a `PostgresEntityMeta` 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 `information_schema`.
|
|
20
|
+
*
|
|
21
|
+
* Writes/reads/deletes consume the meta directly: no per-call cache
|
|
22
|
+
* gate. If the table is not provisioned, Postgres returns
|
|
23
|
+
* `undefined_table` (`42P01`); the standard `storageFailure` wrap
|
|
24
|
+
* surfaces it per entry.
|
|
25
|
+
*
|
|
26
|
+
* `fn=ls` / `fn=count` push down to SQL — the shallow-direct-leaves
|
|
27
|
+
* predicate (`uri LIKE prefix% AND uri NOT LIKE prefix%/%`) is
|
|
28
|
+
* enforced in the WHERE clause against the entity's table.
|
|
29
|
+
*
|
|
30
|
+
* Writes are wrapped in a transaction (`capabilities.atomicBatch =
|
|
31
|
+
* true`). Stream-shaped values on `BYTEA` columns are collected to
|
|
32
|
+
* `Uint8Array` *before* the transaction opens, so a stream failure
|
|
33
|
+
* can't leave a half-applied commit.
|
|
34
|
+
*/
|
|
35
|
+
import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.js";
|
|
36
|
+
import type { EntityStore } from "../entity-store.js";
|
|
37
|
+
import type { EntityMeta, EntityRecord, EntitySchema } from "../entity.js";
|
|
38
|
+
import type { StoreCapabilities, StoreWriteResult } from "../types.js";
|
|
39
|
+
import { type ColumnPlan } from "./columns.js";
|
|
40
|
+
import type { SqlExecutor } from "./mod.js";
|
|
41
|
+
/**
|
|
42
|
+
* PostgresStore-specific entity handle.
|
|
43
|
+
*
|
|
44
|
+
* `tableName` is the prefixed-and-suffixed target table; `columns`
|
|
45
|
+
* are the planned user columns (without the always-present `uri`,
|
|
46
|
+
* `created_at`, `updated_at`); `declared` is the set of user column
|
|
47
|
+
* names for the per-write extras check; `bytesColumns` drives
|
|
48
|
+
* pre-transaction stream normalisation.
|
|
49
|
+
*/
|
|
50
|
+
export interface PostgresEntityMeta extends EntityMeta {
|
|
51
|
+
readonly tableName: string;
|
|
52
|
+
readonly columns: readonly ColumnPlan[];
|
|
53
|
+
readonly declared: ReadonlySet<string>;
|
|
54
|
+
readonly bytesColumns: ReadonlySet<string>;
|
|
55
|
+
}
|
|
56
|
+
export declare class PostgresStore implements EntityStore<PostgresEntityMeta> {
|
|
57
|
+
private readonly tablePrefix;
|
|
58
|
+
private readonly executor;
|
|
59
|
+
constructor(tablePrefix: string, executor: SqlExecutor);
|
|
60
|
+
entitySupport(schema: EntitySchema): PostgresEntityMeta;
|
|
61
|
+
entityStatus(meta: PostgresEntityMeta): Promise<"live" | "unprovisioned">;
|
|
62
|
+
provisionEntity(meta: PostgresEntityMeta): Promise<void>;
|
|
63
|
+
write(meta: PostgresEntityMeta, entries: {
|
|
64
|
+
uri: string;
|
|
65
|
+
record: EntityRecord;
|
|
66
|
+
}[]): Promise<StoreWriteResult[]>;
|
|
67
|
+
read<T = EntityRecord | undefined>(meta: PostgresEntityMeta, urls: string[]): Promise<Output<T>[]>;
|
|
68
|
+
delete(meta: PostgresEntityMeta, uris: string[]): Promise<DeleteResult[]>;
|
|
69
|
+
status(): Promise<StatusResult>;
|
|
70
|
+
capabilities(): StoreCapabilities;
|
|
71
|
+
private _readOne;
|
|
72
|
+
private _ls;
|
|
73
|
+
private _count;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/postgres/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAM3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,KAAK,UAAU,EAAe,MAAM,cAAc,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM5C;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC5C;AAoCD,qBAAa,aAAc,YAAW,WAAW,CAAC,kBAAkB,CAAC;IACnE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;gBAE3B,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW;IAetD,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB;IAkBjD,YAAY,CAChB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IA4B9B,eAAe,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBxD,KAAK,CACT,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA4E9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAQjB,MAAM,CACV,IAAI,EAAE,kBAAkB,EACxB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,YAAY,EAAE,CAAC;IAkBpB,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAoBrC,YAAY,IAAI,iBAAiB;YAMnB,QAAQ;YAqBR,GAAG;YAgCH,MAAM;CAcrB"}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgresStore — PostgreSQL implementation of `EntityStore`.
|
|
3
|
+
*
|
|
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 BYTEA` column — no legacy
|
|
8
|
+
* special case.
|
|
9
|
+
*
|
|
10
|
+
* ## Lifecycle
|
|
11
|
+
*
|
|
12
|
+
* `entitySupport(schema)` is pure — it runs `planColumns` and returns
|
|
13
|
+
* a `PostgresEntityMeta` 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 `information_schema`.
|
|
20
|
+
*
|
|
21
|
+
* Writes/reads/deletes consume the meta directly: no per-call cache
|
|
22
|
+
* gate. If the table is not provisioned, Postgres returns
|
|
23
|
+
* `undefined_table` (`42P01`); the standard `storageFailure` wrap
|
|
24
|
+
* surfaces it per entry.
|
|
25
|
+
*
|
|
26
|
+
* `fn=ls` / `fn=count` push down to SQL — the shallow-direct-leaves
|
|
27
|
+
* predicate (`uri LIKE prefix% AND uri NOT LIKE prefix%/%`) is
|
|
28
|
+
* enforced in the WHERE clause against the entity's table.
|
|
29
|
+
*
|
|
30
|
+
* Writes are wrapped in a transaction (`capabilities.atomicBatch =
|
|
31
|
+
* true`). Stream-shaped values on `BYTEA` columns are collected to
|
|
32
|
+
* `Uint8Array` *before* the transaction opens, so a stream failure
|
|
33
|
+
* can't leave a half-applied commit.
|
|
34
|
+
*/
|
|
35
|
+
import { dispatchRead } from "../dispatch.js";
|
|
36
|
+
import { storageFailure } from "../errors.js";
|
|
37
|
+
import { toBytes } from "../payload.js";
|
|
38
|
+
import { validateReadParams } from "../read.js";
|
|
39
|
+
import { planColumns } from "./columns.js";
|
|
40
|
+
const STORE_NAME = "PostgresStore";
|
|
41
|
+
const TABLE_PREFIX = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
42
|
+
/** Map a `ColumnPlan.sqlType` to the `information_schema.columns.data_type` value. */
|
|
43
|
+
const INFO_SCHEMA_TYPE = {
|
|
44
|
+
TEXT: "text",
|
|
45
|
+
"DOUBLE PRECISION": "double precision",
|
|
46
|
+
BIGINT: "bigint",
|
|
47
|
+
BOOLEAN: "boolean",
|
|
48
|
+
BYTEA: "bytea",
|
|
49
|
+
TIMESTAMPTZ: "timestamp with time zone",
|
|
50
|
+
JSONB: "jsonb",
|
|
51
|
+
};
|
|
52
|
+
/** Coerce a pg `bytea` row value into a `Uint8Array`. */
|
|
53
|
+
function rowToBytes(value) {
|
|
54
|
+
if (value instanceof Uint8Array)
|
|
55
|
+
return value;
|
|
56
|
+
if (typeof value === "string" && value.startsWith("\\x")) {
|
|
57
|
+
const hex = value.slice(2);
|
|
58
|
+
const out = new Uint8Array(hex.length / 2);
|
|
59
|
+
for (let i = 0; i < out.length; i++) {
|
|
60
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`${STORE_NAME}: unexpected payload type ${typeof value}`);
|
|
65
|
+
}
|
|
66
|
+
function entityTableName(tablePrefix, entityName) {
|
|
67
|
+
if (!TABLE_PREFIX.test(entityName)) {
|
|
68
|
+
throw new Error(`${STORE_NAME}: entity name '${entityName}' must match ${TABLE_PREFIX.source}`);
|
|
69
|
+
}
|
|
70
|
+
return `${tablePrefix}_${entityName}_data`;
|
|
71
|
+
}
|
|
72
|
+
export class PostgresStore {
|
|
73
|
+
tablePrefix;
|
|
74
|
+
executor;
|
|
75
|
+
constructor(tablePrefix, executor) {
|
|
76
|
+
if (!tablePrefix)
|
|
77
|
+
throw new Error("tablePrefix is required");
|
|
78
|
+
if (!TABLE_PREFIX.test(tablePrefix)) {
|
|
79
|
+
throw new Error(`tablePrefix must match ${TABLE_PREFIX.source}; got '${tablePrefix}'`);
|
|
80
|
+
}
|
|
81
|
+
if (!executor)
|
|
82
|
+
throw new Error("executor is required");
|
|
83
|
+
this.tablePrefix = tablePrefix;
|
|
84
|
+
this.executor = executor;
|
|
85
|
+
}
|
|
86
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
87
|
+
entitySupport(schema) {
|
|
88
|
+
const { columns, unsupported } = planColumns(schema.fields);
|
|
89
|
+
const support = {
|
|
90
|
+
entity: schema.name,
|
|
91
|
+
supported: columns.map((c) => c.name),
|
|
92
|
+
unsupported,
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
support,
|
|
96
|
+
tableName: entityTableName(this.tablePrefix, schema.name),
|
|
97
|
+
columns,
|
|
98
|
+
declared: new Set(columns.map((c) => c.name)),
|
|
99
|
+
bytesColumns: new Set(columns.filter((c) => c.sqlType === "BYTEA").map((c) => c.name)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async entityStatus(meta) {
|
|
103
|
+
const res = await this.executor.query(`SELECT column_name, data_type FROM information_schema.columns
|
|
104
|
+
WHERE table_name = $1`, [meta.tableName]);
|
|
105
|
+
const rows = (res.rows ?? []);
|
|
106
|
+
if (rows.length === 0)
|
|
107
|
+
return "unprovisioned";
|
|
108
|
+
const actual = new Map(rows.map((r) => [r.column_name, r.data_type]));
|
|
109
|
+
for (const c of meta.columns) {
|
|
110
|
+
const got = actual.get(c.name);
|
|
111
|
+
if (got === undefined)
|
|
112
|
+
return "unprovisioned";
|
|
113
|
+
const expected = INFO_SCHEMA_TYPE[c.sqlType];
|
|
114
|
+
if (expected !== undefined && got !== expected)
|
|
115
|
+
return "unprovisioned";
|
|
116
|
+
}
|
|
117
|
+
// Reject collisions in the other direction: the live table carrying
|
|
118
|
+
// user columns the meta does not declare is a different shape.
|
|
119
|
+
for (const name of actual.keys()) {
|
|
120
|
+
if (name === "uri" || name === "created_at" || name === "updated_at") {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!meta.declared.has(name))
|
|
124
|
+
return "unprovisioned";
|
|
125
|
+
}
|
|
126
|
+
return "live";
|
|
127
|
+
}
|
|
128
|
+
async provisionEntity(meta) {
|
|
129
|
+
const colDdl = meta.columns
|
|
130
|
+
.map((c) => `"${c.name}" ${c.sqlType}`)
|
|
131
|
+
.join(",\n ");
|
|
132
|
+
const sql = `CREATE TABLE IF NOT EXISTS ${meta.tableName} (
|
|
133
|
+
uri TEXT PRIMARY KEY${meta.columns.length > 0 ? ",\n " + colDdl : ""},
|
|
134
|
+
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
|
135
|
+
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
|
136
|
+
);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_${meta.tableName}_uri ON ${meta.tableName} (uri);`;
|
|
138
|
+
await this.executor.query(sql);
|
|
139
|
+
// If the table already existed with a different shape, the
|
|
140
|
+
// CREATE IF NOT EXISTS was a no-op — surface the collision.
|
|
141
|
+
const status = await this.entityStatus(meta);
|
|
142
|
+
if (status !== "live") {
|
|
143
|
+
throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
|
|
144
|
+
`provisioned with a different shape at table '${meta.tableName}'`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ── Write ────────────────────────────────────────────────────────
|
|
148
|
+
async write(meta, entries) {
|
|
149
|
+
if (entries.length === 0)
|
|
150
|
+
return [];
|
|
151
|
+
const out = new Array(entries.length);
|
|
152
|
+
const accepted = [];
|
|
153
|
+
// Validate + collect stream-shaped bytes fields BEFORE the
|
|
154
|
+
// transaction. Per-entry validation errors leak as failures in
|
|
155
|
+
// their slots; everything else gets committed atomically below.
|
|
156
|
+
for (let i = 0; i < entries.length; i++) {
|
|
157
|
+
const { uri, record } = entries[i];
|
|
158
|
+
const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
|
|
159
|
+
if (extras.length > 0) {
|
|
160
|
+
out[i] = {
|
|
161
|
+
success: false,
|
|
162
|
+
...storageFailure(new Error(`${STORE_NAME}: record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
|
|
163
|
+
};
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const normalised = await normaliseBytesFields(record, meta.bytesColumns);
|
|
168
|
+
accepted.push({ idx: i, uri, record: normalised });
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
out[i] = {
|
|
172
|
+
success: false,
|
|
173
|
+
...storageFailure(err, "Invalid record", uri),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (accepted.length === 0)
|
|
178
|
+
return out;
|
|
179
|
+
const cols = meta.columns;
|
|
180
|
+
const colList = cols.map((c) => `"${c.name}"`).join(", ");
|
|
181
|
+
const placeholders = cols.map((_, i) => `$${i + 2}`).join(", ");
|
|
182
|
+
const updates = cols
|
|
183
|
+
.map((c) => `"${c.name}" = EXCLUDED."${c.name}"`)
|
|
184
|
+
.concat(["updated_at = CURRENT_TIMESTAMP"])
|
|
185
|
+
.join(", ");
|
|
186
|
+
try {
|
|
187
|
+
await this.executor.transaction(async (tx) => {
|
|
188
|
+
for (const { uri, record } of accepted) {
|
|
189
|
+
const args = [uri];
|
|
190
|
+
for (const c of cols)
|
|
191
|
+
args.push(adaptForWrite(c, record[c.name]));
|
|
192
|
+
await tx.query(`INSERT INTO ${meta.tableName} (uri${colList ? ", " + colList : ""}) VALUES ($1${placeholders ? ", " + placeholders : ""})
|
|
193
|
+
ON CONFLICT (uri) DO UPDATE SET ${updates}`, args);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
for (const { idx } of accepted)
|
|
197
|
+
out[idx] = { success: true };
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
// Atomic batch failure — every accepted entry shares the same failure.
|
|
202
|
+
// An undefined_table from a not-yet-provisioned meta lands here too.
|
|
203
|
+
const failure = storageFailure(err, "Write failed");
|
|
204
|
+
for (const { idx } of accepted)
|
|
205
|
+
out[idx] = { success: false, ...failure };
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ── Read ─────────────────────────────────────────────────────────
|
|
210
|
+
read(meta, urls) {
|
|
211
|
+
return dispatchRead(urls, STORE_NAME, {
|
|
212
|
+
read: (p) => this._readOne(meta, p.uri),
|
|
213
|
+
ls: (p) => this._ls(meta, p),
|
|
214
|
+
count: (p) => this._count(meta, p),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async delete(meta, uris) {
|
|
218
|
+
if (uris.length === 0)
|
|
219
|
+
return [];
|
|
220
|
+
try {
|
|
221
|
+
await this.executor.transaction(async (tx) => {
|
|
222
|
+
for (const uri of uris) {
|
|
223
|
+
await tx.query(`DELETE FROM ${meta.tableName} WHERE uri = $1`, [uri]);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return uris.map(() => ({ success: true }));
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
const failure = storageFailure(err, "Delete failed");
|
|
230
|
+
return uris.map(() => ({ success: false, ...failure }));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async status() {
|
|
234
|
+
try {
|
|
235
|
+
await this.executor.query("SELECT 1");
|
|
236
|
+
return {
|
|
237
|
+
status: "healthy",
|
|
238
|
+
message: "PostgreSQL store is operational",
|
|
239
|
+
fns: ["read", "ls", "count"],
|
|
240
|
+
details: { tablePrefix: this.tablePrefix },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return {
|
|
245
|
+
status: "unhealthy",
|
|
246
|
+
message: `PostgreSQL health check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
247
|
+
fns: ["read", "ls", "count"],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
capabilities() {
|
|
252
|
+
return { atomicBatch: true };
|
|
253
|
+
}
|
|
254
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
255
|
+
async _readOne(meta, uri) {
|
|
256
|
+
if (meta.columns.length === 0) {
|
|
257
|
+
// Empty schema: presence-only — return an empty record on hit.
|
|
258
|
+
const res = await this.executor.query(`SELECT 1 FROM ${meta.tableName} WHERE uri = $1`, [uri]);
|
|
259
|
+
return res.rows && res.rows.length > 0 ? {} : undefined;
|
|
260
|
+
}
|
|
261
|
+
const cols = meta.columns.map((c) => `"${c.name}"`).join(", ");
|
|
262
|
+
const res = await this.executor.query(`SELECT ${cols} FROM ${meta.tableName} WHERE uri = $1`, [uri]);
|
|
263
|
+
if (!res.rows || res.rows.length === 0)
|
|
264
|
+
return undefined;
|
|
265
|
+
return adaptRowForRead(meta, res.rows[0]);
|
|
266
|
+
}
|
|
267
|
+
async _ls(meta, parsed) {
|
|
268
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
269
|
+
const { params } = parsed;
|
|
270
|
+
const format = params.format ?? "full";
|
|
271
|
+
const cols = meta.columns.length > 0
|
|
272
|
+
? "uri, " + meta.columns.map((c) => `"${c.name}"`).join(", ")
|
|
273
|
+
: "uri";
|
|
274
|
+
const selectClause = format === "uris" ? "uri" : cols;
|
|
275
|
+
const order = params.sortBy === "uri"
|
|
276
|
+
? ` ORDER BY uri ${params.sortOrder === "desc" ? "DESC" : "ASC"}`
|
|
277
|
+
: "";
|
|
278
|
+
let sql = `SELECT ${selectClause} FROM ${meta.tableName} WHERE uri LIKE $1 || '%' AND uri NOT LIKE $1 || '%/%'${order}`;
|
|
279
|
+
const args = [parsed.uri];
|
|
280
|
+
if (params.limit !== undefined) {
|
|
281
|
+
const page = params.page ?? 1;
|
|
282
|
+
args.push(params.limit, (page - 1) * params.limit);
|
|
283
|
+
sql += ` LIMIT $${args.length - 1} OFFSET $${args.length}`;
|
|
284
|
+
}
|
|
285
|
+
const res = await this.executor.query(sql, args);
|
|
286
|
+
const rows = (res.rows ?? []);
|
|
287
|
+
if (format === "uris")
|
|
288
|
+
return rows.map((r) => r.uri);
|
|
289
|
+
return rows.map((r) => [
|
|
290
|
+
r.uri,
|
|
291
|
+
adaptRowForRead(meta, r),
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
async _count(meta, parsed) {
|
|
295
|
+
if (parsed.params.pattern !== undefined) {
|
|
296
|
+
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
297
|
+
}
|
|
298
|
+
const res = await this.executor.query(`SELECT COUNT(*)::int AS n FROM ${meta.tableName} WHERE uri LIKE $1 || '%' AND uri NOT LIKE $1 || '%/%'`, [parsed.uri]);
|
|
299
|
+
const row = res.rows?.[0];
|
|
300
|
+
return row?.n ?? 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Collect any `ReadableStream` values on `BYTEA` columns into
|
|
305
|
+
* `Uint8Array` and return a shallow-copied record. Runs before the
|
|
306
|
+
* write transaction so stream failures can't leave a half-applied
|
|
307
|
+
* commit. Non-stream values pass through verbatim.
|
|
308
|
+
*/
|
|
309
|
+
async function normaliseBytesFields(record, bytesColumns) {
|
|
310
|
+
if (bytesColumns.size === 0)
|
|
311
|
+
return record;
|
|
312
|
+
const out = { ...record };
|
|
313
|
+
for (const name of bytesColumns) {
|
|
314
|
+
const v = out[name];
|
|
315
|
+
if (v === undefined || v === null)
|
|
316
|
+
continue;
|
|
317
|
+
if (v instanceof Uint8Array)
|
|
318
|
+
continue;
|
|
319
|
+
if (v instanceof ReadableStream) {
|
|
320
|
+
out[name] = await toBytes(v);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
throw new Error(`${STORE_NAME}: field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
/** Coerce a record field value into something the driver will accept. */
|
|
328
|
+
function adaptForWrite(col, value) {
|
|
329
|
+
if (value === undefined || value === null)
|
|
330
|
+
return null;
|
|
331
|
+
if (col.sqlType === "JSONB") {
|
|
332
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
333
|
+
}
|
|
334
|
+
if (col.sqlType === "TIMESTAMPTZ") {
|
|
335
|
+
if (value instanceof Date)
|
|
336
|
+
return value.toISOString();
|
|
337
|
+
if (typeof value === "number")
|
|
338
|
+
return new Date(value).toISOString();
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
/** Reconstruct an EntityRecord from a Postgres row. */
|
|
344
|
+
function adaptRowForRead(meta, row) {
|
|
345
|
+
const rec = {};
|
|
346
|
+
for (const col of meta.columns) {
|
|
347
|
+
const v = row[col.name];
|
|
348
|
+
if (v === null || v === undefined) {
|
|
349
|
+
rec[col.name] = undefined;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (col.sqlType === "BYTEA")
|
|
353
|
+
rec[col.name] = rowToBytes(v);
|
|
354
|
+
else if (col.sqlType === "JSONB") {
|
|
355
|
+
rec[col.name] = typeof v === "string" ? JSON.parse(v) : v;
|
|
356
|
+
}
|
|
357
|
+
else if (col.sqlType === "TIMESTAMPTZ") {
|
|
358
|
+
rec[col.name] = v instanceof Date ? v : new Date(v);
|
|
359
|
+
}
|
|
360
|
+
else
|
|
361
|
+
rec[col.name] = v;
|
|
362
|
+
}
|
|
363
|
+
return rec;
|
|
364
|
+
}
|
package/esm/read.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process post-processing for `fn=ls` results.
|
|
3
|
+
*
|
|
4
|
+
* Stores that cannot push `sortBy`/`limit`/`page`/`format` down to
|
|
5
|
+
* their backend collect the raw `Output[]` rows under a prefix and
|
|
6
|
+
* pipe them through `applyReadParams`. Stores that CAN push these
|
|
7
|
+
* down (postgres, mongo, elasticsearch, s3) should skip this helper
|
|
8
|
+
* and handle the params in their query.
|
|
9
|
+
*
|
|
10
|
+
* Throws on unsupported params — programmer errors are not silent
|
|
11
|
+
* "misses." See project decisions in `project_core_upgrade.md`.
|
|
12
|
+
*/
|
|
13
|
+
import type { Output } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.js";
|
|
14
|
+
import type { ReadParams } from "./url.js";
|
|
15
|
+
/**
|
|
16
|
+
* Validate standard ReadParams and throw on anything we cannot honor.
|
|
17
|
+
*
|
|
18
|
+
* Push-down stores (postgres, mongo, ES, S3) call this first, then
|
|
19
|
+
* translate the surviving params into their backend query language.
|
|
20
|
+
* Stores that post-process in memory call `applyReadParams` instead,
|
|
21
|
+
* which validates and applies in one go.
|
|
22
|
+
*
|
|
23
|
+
* Project-wide baseline: `pattern` and `cursor` are unsupported
|
|
24
|
+
* everywhere; `sortBy` only accepts `"uri"`; `format` only accepts
|
|
25
|
+
* `"full"` (default) or `"uris"`. Per-store relaxations should be
|
|
26
|
+
* added explicitly as features land.
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateReadParams(params: ReadParams, storeName: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Apply standard ReadParams to a list of rows.
|
|
31
|
+
*
|
|
32
|
+
* @param rows raw `[uri, payload]` entries collected from the backend
|
|
33
|
+
* @param params parsed read params (from `parseUrl(...).params`)
|
|
34
|
+
* @param storeName label used in thrown error messages
|
|
35
|
+
*
|
|
36
|
+
* Returns `Output[]` when `format` is `"full"` (default) or `string[]`
|
|
37
|
+
* when `format` is `"uris"`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyReadParams<T>(rows: Output<T>[], params: ReadParams, storeName: string): Output<T>[] | string[];
|
|
40
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../src/read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kEAAkE,CAAC;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,GAChB,IAAI,CAcN;AAED;;;;;;;;;GASG;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,CAkBxB"}
|