@btravstack/storage 0.3.0
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/README.md +133 -0
- package/dist/index.cjs +144 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.mts +56 -0
- package/dist/index.mjs +135 -0
- package/dist/s3.cjs +103 -0
- package/dist/s3.d.cts +45 -0
- package/dist/s3.d.mts +45 -0
- package/dist/s3.mjs +99 -0
- package/dist/storage-C9j3zj7r.d.cts +61 -0
- package/dist/storage-C9j3zj7r.d.mts +61 -0
- package/dist/storage-CRkwIhac.mjs +25 -0
- package/dist/storage-QVPXOcO6.cjs +54 -0
- package/package.json +102 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Benoit TRAVERS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @btravstack/storage
|
|
2
|
+
|
|
3
|
+
> The object-storage port for [`@btravstack/core`](../core): one `Storage` an
|
|
4
|
+
> application depends on, an in-memory adapter, an S3-compatible one with
|
|
5
|
+
> presigned reads, and a span, a count and a log line per operation.
|
|
6
|
+
|
|
7
|
+
๐ **[Documentation](https://btravstack.github.io/btravstack/reference/storage)** ยท
|
|
8
|
+
[API Reference](https://btravstack.github.io/btravstack/api/storage/)
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @btravstack/storage @btravstack/core @btravstack/config @btravstack/di unthrown
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Four peer dependencies, plus `@aws-sdk/client-s3` and
|
|
15
|
+
`@aws-sdk/s3-request-presigner` โ optional, and needed only if you compose the
|
|
16
|
+
S3 adapter. Instrumentation needs no peer of its own: the `Logger`, `Tracer`
|
|
17
|
+
and `Meter` it depends on are
|
|
18
|
+
[the kernel's ports](https://btravstack.github.io/btravstack/reference/core/observability).
|
|
19
|
+
Node `>=20`.
|
|
20
|
+
|
|
21
|
+
## A worked example
|
|
22
|
+
|
|
23
|
+
<!-- doctest: group=order-temporal-worker -->
|
|
24
|
+
<!-- doctest: prelude
|
|
25
|
+
import { Module } from "@btravstack/di";
|
|
26
|
+
import { Logger } from "@btravstack/core";
|
|
27
|
+
import { observability } from "@btravstack/observability";
|
|
28
|
+
import { otel } from "@btravstack/observability/otel";
|
|
29
|
+
import { P } from "unthrown";
|
|
30
|
+
-->
|
|
31
|
+
|
|
32
|
+
Storing a document, and reading it back through a URL that carries no
|
|
33
|
+
credentials:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {
|
|
37
|
+
Port as InvoicePort,
|
|
38
|
+
Provider as InvoiceProvider,
|
|
39
|
+
} from "@btravstack/di";
|
|
40
|
+
import { Storage } from "@btravstack/storage";
|
|
41
|
+
import type { AsyncResult } from "unthrown";
|
|
42
|
+
|
|
43
|
+
class Invoices extends InvoicePort("ReadmeInvoices")<{
|
|
44
|
+
readonly store: (
|
|
45
|
+
tenantId: string,
|
|
46
|
+
id: string,
|
|
47
|
+
pdf: Uint8Array,
|
|
48
|
+
) => AsyncResult<void, never>;
|
|
49
|
+
readonly link: (
|
|
50
|
+
tenantId: string,
|
|
51
|
+
id: string,
|
|
52
|
+
) => AsyncResult<string | undefined, never>;
|
|
53
|
+
}> {}
|
|
54
|
+
|
|
55
|
+
// The tenant is in the key because the port has no slot for one โ the same
|
|
56
|
+
// rule a cache key follows.
|
|
57
|
+
const keyFor = (tenantId: string, id: string) =>
|
|
58
|
+
`invoices/${tenantId}/${id}.pdf`;
|
|
59
|
+
|
|
60
|
+
export const invoices = InvoiceProvider(Invoices)(
|
|
61
|
+
{ storage: Storage },
|
|
62
|
+
{
|
|
63
|
+
sync: ({ storage }) => ({
|
|
64
|
+
store: (tenantId, id, pdf) =>
|
|
65
|
+
storage
|
|
66
|
+
.put(keyFor(tenantId, id), pdf, { contentType: "application/pdf" })
|
|
67
|
+
// A document that failed to store is YOUR decision to make; here it
|
|
68
|
+
// is swallowed because the instrumented store already logged and
|
|
69
|
+
// counted it one layer down.
|
|
70
|
+
.recoverErrCases((matcher) =>
|
|
71
|
+
matcher.with(P.tag("StorageUnavailable"), () => undefined),
|
|
72
|
+
),
|
|
73
|
+
// Serving bytes through your own process is the anti-pattern this arm
|
|
74
|
+
// exists to avoid: the caller fetches the object directly, for a minute.
|
|
75
|
+
link: (tenantId, id) =>
|
|
76
|
+
storage
|
|
77
|
+
.presignedUrl(keyFor(tenantId, id), { ttlMs: 60_000 })
|
|
78
|
+
.recoverErrCases((matcher) =>
|
|
79
|
+
matcher
|
|
80
|
+
.with(P.tag("StorageUnavailable"), () => undefined)
|
|
81
|
+
.with(P.tag("PresignNotSupported"), () => undefined),
|
|
82
|
+
),
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The composition โ the adapter, and whether operations are instrumented,
|
|
89
|
+
decided here and nowhere else:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { storage } from "@btravstack/storage";
|
|
93
|
+
import { s3Storage } from "@btravstack/storage/s3";
|
|
94
|
+
|
|
95
|
+
export const InvoicesApp = Module("InvoicesApp")({
|
|
96
|
+
imports: [
|
|
97
|
+
// Instrumented by default, which is why `observability()` and `otel()`
|
|
98
|
+
// are below. Pass `instrumented: false` for the same graph without them.
|
|
99
|
+
storage({ adapter: s3Storage() }),
|
|
100
|
+
observability(),
|
|
101
|
+
otel(),
|
|
102
|
+
],
|
|
103
|
+
provides: [invoices],
|
|
104
|
+
exports: [Invoices, Logger],
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Options
|
|
109
|
+
|
|
110
|
+
| Option | Where | What it is |
|
|
111
|
+
| ----------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------- |
|
|
112
|
+
| `adapter` | `storage({ adapter })` | the adapter module providing `StorageBackend` โ required |
|
|
113
|
+
| `instrumented` | `storage({ instrumented })` | span, count and log every operation (default `true`; `false` opts out) |
|
|
114
|
+
| `STORAGE_S3_ENDPOINT` | environment, read by `s3Storage()` | the store's endpoint โ required |
|
|
115
|
+
| `STORAGE_S3_BUCKET` | environment | the bucket โ required |
|
|
116
|
+
| `STORAGE_S3_ACCESS_KEY_ID` / `STORAGE_S3_SECRET_ACCESS_KEY` | environment | the credentials โ required |
|
|
117
|
+
| `STORAGE_S3_REGION` | environment | default `us-east-1` |
|
|
118
|
+
|
|
119
|
+
The full table โ defaults, semantics and the reasoning โ lives on
|
|
120
|
+
[the reference page](https://btravstack.github.io/btravstack/reference/storage),
|
|
121
|
+
which is this list's one detailed home.
|
|
122
|
+
|
|
123
|
+
## What it decides, and what it does not
|
|
124
|
+
|
|
125
|
+
**It decides** that a missing object is an ordinary answer rather than a fault
|
|
126
|
+
(counted `not_found`, logged at `info`), that `delete` is idempotent, that
|
|
127
|
+
presigning asks the store nothing โ and that an adapter which cannot presign
|
|
128
|
+
says so instead of minting a URL that would fail only in production.
|
|
129
|
+
|
|
130
|
+
**It does not decide** what your keys look like, whether a failed write should
|
|
131
|
+
fail your request, or how long a link should live. There is no streaming, no
|
|
132
|
+
presigned writes, no listing and no bucket management; the reasons are in
|
|
133
|
+
[`CLAUDE.md`](./CLAUDE.md).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_storage = require("./storage-QVPXOcO6.cjs");
|
|
3
|
+
let _btravstack_di = require("@btravstack/di");
|
|
4
|
+
let unthrown = require("unthrown");
|
|
5
|
+
let _btravstack_core = require("@btravstack/core");
|
|
6
|
+
//#region src/memory.ts
|
|
7
|
+
/**
|
|
8
|
+
* The in-process adapter: a `Map`, and an honest refusal to presign.
|
|
9
|
+
*
|
|
10
|
+
* `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
|
|
11
|
+
* which would be the worst kind of double โ one that passes locally and fails in
|
|
12
|
+
* the deployment for a reason no test could have shown.
|
|
13
|
+
*
|
|
14
|
+
* ponytail: no size limit, so a process storing unbounded objects grows
|
|
15
|
+
* unbounded. The upgrade path is the S3 adapter.
|
|
16
|
+
*/
|
|
17
|
+
const memoryStorageBackend = () => {
|
|
18
|
+
const objects = /* @__PURE__ */ new Map();
|
|
19
|
+
return {
|
|
20
|
+
put: (key, bytes, options) => {
|
|
21
|
+
objects.set(key, {
|
|
22
|
+
bytes,
|
|
23
|
+
contentType: options.contentType
|
|
24
|
+
});
|
|
25
|
+
return (0, unthrown.OkAsync)();
|
|
26
|
+
},
|
|
27
|
+
get: (key) => {
|
|
28
|
+
const object = objects.get(key);
|
|
29
|
+
return object === void 0 ? (0, unthrown.ErrAsync)(new require_storage.ObjectNotFound({ key })) : (0, unthrown.OkAsync)(object);
|
|
30
|
+
},
|
|
31
|
+
delete: (key) => {
|
|
32
|
+
objects.delete(key);
|
|
33
|
+
return (0, unthrown.OkAsync)();
|
|
34
|
+
},
|
|
35
|
+
presignedUrl: (key) => (0, unthrown.ErrAsync)(new require_storage.PresignNotSupported({ key }))
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
|
|
39
|
+
const memoryStorageProvider = () => (0, _btravstack_di.Provider)(require_storage.StorageBackend)({}, { sync: () => memoryStorageBackend() });
|
|
40
|
+
/** The adapter as a module, which is the shape `storage({ adapter })` takes. */
|
|
41
|
+
const memoryStorage = () => (0, _btravstack_di.Module)("MemoryStorage")({
|
|
42
|
+
provides: [memoryStorageProvider()],
|
|
43
|
+
exports: [require_storage.StorageBackend]
|
|
44
|
+
});
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/instrument.ts
|
|
47
|
+
/**
|
|
48
|
+
* One operation, wrapped: a span around it, a count of how it came out, and a
|
|
49
|
+
* log line if it failed.
|
|
50
|
+
*
|
|
51
|
+
* **A missing object is counted `not_found` and logged at `info`, not
|
|
52
|
+
* `error`.** Asking for something that is not there is an ordinary answer, and
|
|
53
|
+
* a dashboard that treats it as a fault teaches its readers to ignore the fault
|
|
54
|
+
* line. `StorageUnavailable` is what pages somebody.
|
|
55
|
+
*/
|
|
56
|
+
const observed = (call, { operation, key, logger, tracer, operations }) => {
|
|
57
|
+
const span = tracer.startSpan(`storage.${operation}`);
|
|
58
|
+
span.setAttributes({ "btravstack.storage.key": key });
|
|
59
|
+
return call().tap(() => {
|
|
60
|
+
operations.add(1, {
|
|
61
|
+
operation,
|
|
62
|
+
outcome: "ok"
|
|
63
|
+
});
|
|
64
|
+
span.end();
|
|
65
|
+
}).tapFailure((failure) => {
|
|
66
|
+
const cause = failure.tag === "Err" ? failure.error : failure.cause;
|
|
67
|
+
const message = failure.tag !== "Err" ? void 0 : failure.error._tag === "ObjectNotFound" ? "the object was not there" : failure.error._tag === "PresignNotSupported" ? "this store cannot mint a url" : void 0;
|
|
68
|
+
operations.add(1, {
|
|
69
|
+
operation,
|
|
70
|
+
outcome: message === void 0 ? "error" : "not_found"
|
|
71
|
+
});
|
|
72
|
+
if (message === void 0) {
|
|
73
|
+
logger.error("the store could not answer", {
|
|
74
|
+
operation,
|
|
75
|
+
key
|
|
76
|
+
}, cause);
|
|
77
|
+
span.setStatus({ code: _btravstack_core.SPAN_STATUS.error });
|
|
78
|
+
} else logger.info(message, {
|
|
79
|
+
operation,
|
|
80
|
+
key
|
|
81
|
+
});
|
|
82
|
+
span.end();
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const instrument = (backend, logger, tracer, meter) => {
|
|
86
|
+
const operations = meter.createCounter("btravstack.storage.operations", { description: "Storage operations, by operation and outcome" });
|
|
87
|
+
const context = (operation, key) => ({
|
|
88
|
+
operation,
|
|
89
|
+
key,
|
|
90
|
+
logger,
|
|
91
|
+
tracer,
|
|
92
|
+
operations
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
put: (key, bytes, options) => observed(() => backend.put(key, bytes, options), context("put", key)),
|
|
96
|
+
get: (key) => observed(() => backend.get(key), context("get", key)),
|
|
97
|
+
delete: (key) => observed(() => backend.delete(key), context("delete", key)),
|
|
98
|
+
presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key))
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/module.ts
|
|
103
|
+
/**
|
|
104
|
+
* The storage starter: an adapter, and `Storage` provided from it โ
|
|
105
|
+
* instrumented or not, decided here at the composition root.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* storage({ adapter: s3Storage() }); // spans, counts, log lines
|
|
109
|
+
* storage({ adapter: s3Storage(), instrumented: false }); // just a store
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* **Two ports, because di allows one provider per port per graph**: the port an
|
|
113
|
+
* application depends on must not be the one an adapter provides, which is also
|
|
114
|
+
* why instrumentation is a flag on the composition rather than a wrapper.
|
|
115
|
+
*/
|
|
116
|
+
const storage = ({ adapter, instrumented }) => instrumented !== false ? (0, _btravstack_di.Module)("InstrumentedStorage")({
|
|
117
|
+
needs: [
|
|
118
|
+
_btravstack_core.Logger,
|
|
119
|
+
_btravstack_core.Meter,
|
|
120
|
+
_btravstack_core.Tracer
|
|
121
|
+
],
|
|
122
|
+
imports: [adapter],
|
|
123
|
+
provides: [(0, _btravstack_di.Provider)(require_storage.Storage)({
|
|
124
|
+
backend: require_storage.StorageBackend,
|
|
125
|
+
logger: _btravstack_core.Logger,
|
|
126
|
+
tracer: _btravstack_core.Tracer,
|
|
127
|
+
meter: _btravstack_core.Meter
|
|
128
|
+
}, { sync: ({ backend, logger, tracer, meter }) => instrument(backend, logger, tracer, meter) })],
|
|
129
|
+
exports: [require_storage.Storage]
|
|
130
|
+
}) : (0, _btravstack_di.Module)("Storage")({
|
|
131
|
+
imports: [adapter],
|
|
132
|
+
provides: [(0, _btravstack_di.Provider)(require_storage.Storage)({ backend: require_storage.StorageBackend }, { sync: ({ backend }) => backend })],
|
|
133
|
+
exports: [require_storage.Storage]
|
|
134
|
+
});
|
|
135
|
+
//#endregion
|
|
136
|
+
exports.ObjectNotFound = require_storage.ObjectNotFound;
|
|
137
|
+
exports.PresignNotSupported = require_storage.PresignNotSupported;
|
|
138
|
+
exports.Storage = require_storage.Storage;
|
|
139
|
+
exports.StorageBackend = require_storage.StorageBackend;
|
|
140
|
+
exports.StorageUnavailable = require_storage.StorageUnavailable;
|
|
141
|
+
exports.memoryStorage = memoryStorage;
|
|
142
|
+
exports.memoryStorageBackend = memoryStorageBackend;
|
|
143
|
+
exports.memoryStorageProvider = memoryStorageProvider;
|
|
144
|
+
exports.storage = storage;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { a as StorageService, i as StorageBackend, n as PresignNotSupported, o as StorageUnavailable, r as Storage, s as StoredObject, t as ObjectNotFound } from "./storage-C9j3zj7r.cjs";
|
|
2
|
+
import { Module, Provider } from "@btravstack/di";
|
|
3
|
+
import { Logger, Meter, Tracer } from "@btravstack/core";
|
|
4
|
+
//#region src/memory.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The in-process adapter: a `Map`, and an honest refusal to presign.
|
|
7
|
+
*
|
|
8
|
+
* `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
|
|
9
|
+
* which would be the worst kind of double โ one that passes locally and fails in
|
|
10
|
+
* the deployment for a reason no test could have shown.
|
|
11
|
+
*
|
|
12
|
+
* ponytail: no size limit, so a process storing unbounded objects grows
|
|
13
|
+
* unbounded. The upgrade path is the S3 adapter.
|
|
14
|
+
*/
|
|
15
|
+
declare const memoryStorageBackend: () => StorageService;
|
|
16
|
+
/** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
|
|
17
|
+
declare const memoryStorageProvider: () => Provider<StorageBackend, never, never> & {
|
|
18
|
+
readonly port: typeof StorageBackend;
|
|
19
|
+
};
|
|
20
|
+
/** The adapter as a module, which is the shape `storage({ adapter })` takes. */
|
|
21
|
+
declare const memoryStorage: () => Module<StorageBackend, never, never>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/module.d.ts
|
|
24
|
+
type StorageOptions<E, N, Instrumented extends boolean> = {
|
|
25
|
+
/**
|
|
26
|
+
* The adapter module: `memoryStorage()` from this entry point,
|
|
27
|
+
* `s3Storage()` from `@btravstack/storage/s3`, or one an application
|
|
28
|
+
* wrote itself over `StorageBackend`.
|
|
29
|
+
*/
|
|
30
|
+
readonly adapter: Module<StorageBackend, E, N>;
|
|
31
|
+
/**
|
|
32
|
+
* Span, count and log every operation. **Default `true`**, `false` opts out.
|
|
33
|
+
*
|
|
34
|
+
* On by default because telemetry that is missing is discovered during an
|
|
35
|
+
* incident. The cost is stated rather than hidden: instrumenting puts
|
|
36
|
+
* `Logger`, `Meter` and `Tracer` in this module's `Needs`, so a root without
|
|
37
|
+
* `observability()` and `otel()` gets a compile error naming all three.
|
|
38
|
+
*/
|
|
39
|
+
readonly instrumented?: Instrumented;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The storage starter: an adapter, and `Storage` provided from it โ
|
|
43
|
+
* instrumented or not, decided here at the composition root.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* storage({ adapter: s3Storage() }); // spans, counts, log lines
|
|
47
|
+
* storage({ adapter: s3Storage(), instrumented: false }); // just a store
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* **Two ports, because di allows one provider per port per graph**: the port an
|
|
51
|
+
* application depends on must not be the one an adapter provides, which is also
|
|
52
|
+
* why instrumentation is a flag on the composition rather than a wrapper.
|
|
53
|
+
*/
|
|
54
|
+
declare const storage: <E, N, Instrumented extends boolean = true>({ adapter, instrumented }: StorageOptions<E, N, Instrumented>) => Module<Storage, E, Instrumented extends true ? N | Logger | Meter | Tracer : N>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { a as StorageService, i as StorageBackend, n as PresignNotSupported, o as StorageUnavailable, r as Storage, s as StoredObject, t as ObjectNotFound } from "./storage-C9j3zj7r.mjs";
|
|
2
|
+
import { Module, Provider } from "@btravstack/di";
|
|
3
|
+
import { Logger, Meter, Tracer } from "@btravstack/core";
|
|
4
|
+
//#region src/memory.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The in-process adapter: a `Map`, and an honest refusal to presign.
|
|
7
|
+
*
|
|
8
|
+
* `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
|
|
9
|
+
* which would be the worst kind of double โ one that passes locally and fails in
|
|
10
|
+
* the deployment for a reason no test could have shown.
|
|
11
|
+
*
|
|
12
|
+
* ponytail: no size limit, so a process storing unbounded objects grows
|
|
13
|
+
* unbounded. The upgrade path is the S3 adapter.
|
|
14
|
+
*/
|
|
15
|
+
declare const memoryStorageBackend: () => StorageService;
|
|
16
|
+
/** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
|
|
17
|
+
declare const memoryStorageProvider: () => Provider<StorageBackend, never, never> & {
|
|
18
|
+
readonly port: typeof StorageBackend;
|
|
19
|
+
};
|
|
20
|
+
/** The adapter as a module, which is the shape `storage({ adapter })` takes. */
|
|
21
|
+
declare const memoryStorage: () => Module<StorageBackend, never, never>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/module.d.ts
|
|
24
|
+
type StorageOptions<E, N, Instrumented extends boolean> = {
|
|
25
|
+
/**
|
|
26
|
+
* The adapter module: `memoryStorage()` from this entry point,
|
|
27
|
+
* `s3Storage()` from `@btravstack/storage/s3`, or one an application
|
|
28
|
+
* wrote itself over `StorageBackend`.
|
|
29
|
+
*/
|
|
30
|
+
readonly adapter: Module<StorageBackend, E, N>;
|
|
31
|
+
/**
|
|
32
|
+
* Span, count and log every operation. **Default `true`**, `false` opts out.
|
|
33
|
+
*
|
|
34
|
+
* On by default because telemetry that is missing is discovered during an
|
|
35
|
+
* incident. The cost is stated rather than hidden: instrumenting puts
|
|
36
|
+
* `Logger`, `Meter` and `Tracer` in this module's `Needs`, so a root without
|
|
37
|
+
* `observability()` and `otel()` gets a compile error naming all three.
|
|
38
|
+
*/
|
|
39
|
+
readonly instrumented?: Instrumented;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The storage starter: an adapter, and `Storage` provided from it โ
|
|
43
|
+
* instrumented or not, decided here at the composition root.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* storage({ adapter: s3Storage() }); // spans, counts, log lines
|
|
47
|
+
* storage({ adapter: s3Storage(), instrumented: false }); // just a store
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* **Two ports, because di allows one provider per port per graph**: the port an
|
|
51
|
+
* application depends on must not be the one an adapter provides, which is also
|
|
52
|
+
* why instrumentation is a flag on the composition rather than a wrapper.
|
|
53
|
+
*/
|
|
54
|
+
declare const storage: <E, N, Instrumented extends boolean = true>({ adapter, instrumented }: StorageOptions<E, N, Instrumented>) => Module<Storage, E, Instrumented extends true ? N | Logger | Meter | Tracer : N>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, type StorageOptions, type StorageService, StorageUnavailable, type StoredObject, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { a as StorageUnavailable, i as StorageBackend, n as PresignNotSupported, r as Storage, t as ObjectNotFound } from "./storage-CRkwIhac.mjs";
|
|
2
|
+
import { Module, Provider } from "@btravstack/di";
|
|
3
|
+
import { ErrAsync, OkAsync } from "unthrown";
|
|
4
|
+
import { Logger, Meter, SPAN_STATUS, Tracer } from "@btravstack/core";
|
|
5
|
+
//#region src/memory.ts
|
|
6
|
+
/**
|
|
7
|
+
* The in-process adapter: a `Map`, and an honest refusal to presign.
|
|
8
|
+
*
|
|
9
|
+
* `presignedUrl` answers `PresignNotSupported` rather than minting a fake URL,
|
|
10
|
+
* which would be the worst kind of double โ one that passes locally and fails in
|
|
11
|
+
* the deployment for a reason no test could have shown.
|
|
12
|
+
*
|
|
13
|
+
* ponytail: no size limit, so a process storing unbounded objects grows
|
|
14
|
+
* unbounded. The upgrade path is the S3 adapter.
|
|
15
|
+
*/
|
|
16
|
+
const memoryStorageBackend = () => {
|
|
17
|
+
const objects = /* @__PURE__ */ new Map();
|
|
18
|
+
return {
|
|
19
|
+
put: (key, bytes, options) => {
|
|
20
|
+
objects.set(key, {
|
|
21
|
+
bytes,
|
|
22
|
+
contentType: options.contentType
|
|
23
|
+
});
|
|
24
|
+
return OkAsync();
|
|
25
|
+
},
|
|
26
|
+
get: (key) => {
|
|
27
|
+
const object = objects.get(key);
|
|
28
|
+
return object === void 0 ? ErrAsync(new ObjectNotFound({ key })) : OkAsync(object);
|
|
29
|
+
},
|
|
30
|
+
delete: (key) => {
|
|
31
|
+
objects.delete(key);
|
|
32
|
+
return OkAsync();
|
|
33
|
+
},
|
|
34
|
+
presignedUrl: (key) => ErrAsync(new PresignNotSupported({ key }))
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/** The adapter as a provider, which is the shape `@btravstack/testing`'s `overridden` takes. */
|
|
38
|
+
const memoryStorageProvider = () => Provider(StorageBackend)({}, { sync: () => memoryStorageBackend() });
|
|
39
|
+
/** The adapter as a module, which is the shape `storage({ adapter })` takes. */
|
|
40
|
+
const memoryStorage = () => Module("MemoryStorage")({
|
|
41
|
+
provides: [memoryStorageProvider()],
|
|
42
|
+
exports: [StorageBackend]
|
|
43
|
+
});
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/instrument.ts
|
|
46
|
+
/**
|
|
47
|
+
* One operation, wrapped: a span around it, a count of how it came out, and a
|
|
48
|
+
* log line if it failed.
|
|
49
|
+
*
|
|
50
|
+
* **A missing object is counted `not_found` and logged at `info`, not
|
|
51
|
+
* `error`.** Asking for something that is not there is an ordinary answer, and
|
|
52
|
+
* a dashboard that treats it as a fault teaches its readers to ignore the fault
|
|
53
|
+
* line. `StorageUnavailable` is what pages somebody.
|
|
54
|
+
*/
|
|
55
|
+
const observed = (call, { operation, key, logger, tracer, operations }) => {
|
|
56
|
+
const span = tracer.startSpan(`storage.${operation}`);
|
|
57
|
+
span.setAttributes({ "btravstack.storage.key": key });
|
|
58
|
+
return call().tap(() => {
|
|
59
|
+
operations.add(1, {
|
|
60
|
+
operation,
|
|
61
|
+
outcome: "ok"
|
|
62
|
+
});
|
|
63
|
+
span.end();
|
|
64
|
+
}).tapFailure((failure) => {
|
|
65
|
+
const cause = failure.tag === "Err" ? failure.error : failure.cause;
|
|
66
|
+
const message = failure.tag !== "Err" ? void 0 : failure.error._tag === "ObjectNotFound" ? "the object was not there" : failure.error._tag === "PresignNotSupported" ? "this store cannot mint a url" : void 0;
|
|
67
|
+
operations.add(1, {
|
|
68
|
+
operation,
|
|
69
|
+
outcome: message === void 0 ? "error" : "not_found"
|
|
70
|
+
});
|
|
71
|
+
if (message === void 0) {
|
|
72
|
+
logger.error("the store could not answer", {
|
|
73
|
+
operation,
|
|
74
|
+
key
|
|
75
|
+
}, cause);
|
|
76
|
+
span.setStatus({ code: SPAN_STATUS.error });
|
|
77
|
+
} else logger.info(message, {
|
|
78
|
+
operation,
|
|
79
|
+
key
|
|
80
|
+
});
|
|
81
|
+
span.end();
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const instrument = (backend, logger, tracer, meter) => {
|
|
85
|
+
const operations = meter.createCounter("btravstack.storage.operations", { description: "Storage operations, by operation and outcome" });
|
|
86
|
+
const context = (operation, key) => ({
|
|
87
|
+
operation,
|
|
88
|
+
key,
|
|
89
|
+
logger,
|
|
90
|
+
tracer,
|
|
91
|
+
operations
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
put: (key, bytes, options) => observed(() => backend.put(key, bytes, options), context("put", key)),
|
|
95
|
+
get: (key) => observed(() => backend.get(key), context("get", key)),
|
|
96
|
+
delete: (key) => observed(() => backend.delete(key), context("delete", key)),
|
|
97
|
+
presignedUrl: (key, options) => observed(() => backend.presignedUrl(key, options), context("presigned_url", key))
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/module.ts
|
|
102
|
+
/**
|
|
103
|
+
* The storage starter: an adapter, and `Storage` provided from it โ
|
|
104
|
+
* instrumented or not, decided here at the composition root.
|
|
105
|
+
*
|
|
106
|
+
* ```ts
|
|
107
|
+
* storage({ adapter: s3Storage() }); // spans, counts, log lines
|
|
108
|
+
* storage({ adapter: s3Storage(), instrumented: false }); // just a store
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* **Two ports, because di allows one provider per port per graph**: the port an
|
|
112
|
+
* application depends on must not be the one an adapter provides, which is also
|
|
113
|
+
* why instrumentation is a flag on the composition rather than a wrapper.
|
|
114
|
+
*/
|
|
115
|
+
const storage = ({ adapter, instrumented }) => instrumented !== false ? Module("InstrumentedStorage")({
|
|
116
|
+
needs: [
|
|
117
|
+
Logger,
|
|
118
|
+
Meter,
|
|
119
|
+
Tracer
|
|
120
|
+
],
|
|
121
|
+
imports: [adapter],
|
|
122
|
+
provides: [Provider(Storage)({
|
|
123
|
+
backend: StorageBackend,
|
|
124
|
+
logger: Logger,
|
|
125
|
+
tracer: Tracer,
|
|
126
|
+
meter: Meter
|
|
127
|
+
}, { sync: ({ backend, logger, tracer, meter }) => instrument(backend, logger, tracer, meter) })],
|
|
128
|
+
exports: [Storage]
|
|
129
|
+
}) : Module("Storage")({
|
|
130
|
+
imports: [adapter],
|
|
131
|
+
provides: [Provider(Storage)({ backend: StorageBackend }, { sync: ({ backend }) => backend })],
|
|
132
|
+
exports: [Storage]
|
|
133
|
+
});
|
|
134
|
+
//#endregion
|
|
135
|
+
export { ObjectNotFound, PresignNotSupported, Storage, StorageBackend, StorageUnavailable, memoryStorage, memoryStorageBackend, memoryStorageProvider, storage };
|
package/dist/s3.cjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_storage = require("./storage-QVPXOcO6.cjs");
|
|
3
|
+
let _btravstack_di = require("@btravstack/di");
|
|
4
|
+
let unthrown = require("unthrown");
|
|
5
|
+
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
6
|
+
let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
7
|
+
let _btravstack_config = require("@btravstack/config");
|
|
8
|
+
//#region src/s3.ts
|
|
9
|
+
/** What the graph bound from the environment for the S3 adapter. */
|
|
10
|
+
var StorageConfig = class extends (0, _btravstack_di.Port)("StorageConfig") {};
|
|
11
|
+
/**
|
|
12
|
+
* The five variables an S3-compatible store needs, and no sixth.
|
|
13
|
+
*
|
|
14
|
+
* `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
|
|
15
|
+
* this port exists for has one, and a default pointing at Amazon would be a
|
|
16
|
+
* surprising bill. **Path-style addressing is not configurable**, it is simply
|
|
17
|
+
* on โ every self-hosted store requires it and AWS accepts it, so it is a value
|
|
18
|
+
* that never changes.
|
|
19
|
+
*/
|
|
20
|
+
const s3Schema = _btravstack_config.Config.object({
|
|
21
|
+
endpoint: _btravstack_config.Config.string("STORAGE_S3_ENDPOINT"),
|
|
22
|
+
region: _btravstack_config.Config.string("STORAGE_S3_REGION", { default: "us-east-1" }),
|
|
23
|
+
bucket: _btravstack_config.Config.string("STORAGE_S3_BUCKET"),
|
|
24
|
+
accessKeyId: _btravstack_config.Config.string("STORAGE_S3_ACCESS_KEY_ID"),
|
|
25
|
+
secretAccessKey: _btravstack_config.Config.string("STORAGE_S3_SECRET_ACCESS_KEY")
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* The client, as a port of its own: a resourceful provider is handed back the
|
|
29
|
+
* service it acquired, and a store's four methods are not something you can
|
|
30
|
+
* close โ so the client rides the graph and the scope closing destroys it.
|
|
31
|
+
*/
|
|
32
|
+
var S3Connection = class extends (0, _btravstack_di.Port)("S3Connection") {};
|
|
33
|
+
const unavailable = (operation, key, cause) => new require_storage.StorageUnavailable({
|
|
34
|
+
operation,
|
|
35
|
+
key,
|
|
36
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
37
|
+
});
|
|
38
|
+
/** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
|
|
39
|
+
const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
|
|
40
|
+
/**
|
|
41
|
+
* The adapter's service over a client and a bucket. A delete of a key nobody
|
|
42
|
+
* stored answers `Ok`, which is S3's own idempotent behaviour rather than a
|
|
43
|
+
* fiction layered over it.
|
|
44
|
+
*/
|
|
45
|
+
const s3StorageBackend = (client, bucket) => ({
|
|
46
|
+
put: (key, bytes, options) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.PutObjectCommand({
|
|
47
|
+
Bucket: bucket,
|
|
48
|
+
Key: key,
|
|
49
|
+
Body: bytes,
|
|
50
|
+
ContentType: options.contentType
|
|
51
|
+
})), (cause) => unavailable("put", key, cause)).map(() => void 0),
|
|
52
|
+
get: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.GetObjectCommand({
|
|
53
|
+
Bucket: bucket,
|
|
54
|
+
Key: key
|
|
55
|
+
})).then(async (response) => ({
|
|
56
|
+
bytes: await response.Body.transformToByteArray(),
|
|
57
|
+
contentType: response.ContentType ?? "application/octet-stream"
|
|
58
|
+
})), (cause) => isMissing(cause) ? new require_storage.ObjectNotFound({ key }) : unavailable("get", key, cause)),
|
|
59
|
+
delete: (key) => (0, unthrown.fromPromise)(client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
60
|
+
Bucket: bucket,
|
|
61
|
+
Key: key
|
|
62
|
+
})), (cause) => unavailable("delete", key, cause)).map(() => void 0),
|
|
63
|
+
presignedUrl: (key, options) => (0, unthrown.fromPromise)((0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
64
|
+
Bucket: bucket,
|
|
65
|
+
Key: key
|
|
66
|
+
}), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause))
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* The S3 adapter: one client, built with the scope and destroyed with it, and
|
|
70
|
+
* `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
|
|
71
|
+
* only through this subpath, so an application composing the memory adapter
|
|
72
|
+
* installs neither.
|
|
73
|
+
*/
|
|
74
|
+
const s3Storage = () => (0, _btravstack_di.Module)("S3Storage")({
|
|
75
|
+
needs: [_btravstack_config.Env],
|
|
76
|
+
provides: [
|
|
77
|
+
_btravstack_config.Config.provider(StorageConfig)(s3Schema),
|
|
78
|
+
(0, _btravstack_di.Provider)(S3Connection)({ config: StorageConfig }, {
|
|
79
|
+
acquire: ({ config }) => (0, unthrown.fromSafePromise)(Promise.resolve({
|
|
80
|
+
client: new _aws_sdk_client_s3.S3Client({
|
|
81
|
+
endpoint: config.endpoint,
|
|
82
|
+
region: config.region,
|
|
83
|
+
forcePathStyle: true,
|
|
84
|
+
credentials: {
|
|
85
|
+
accessKeyId: config.accessKeyId,
|
|
86
|
+
secretAccessKey: config.secretAccessKey
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
bucket: config.bucket
|
|
90
|
+
})),
|
|
91
|
+
release: ({ client }) => {
|
|
92
|
+
client.destroy();
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
(0, _btravstack_di.Provider)(require_storage.StorageBackend)({ connection: S3Connection }, { sync: ({ connection }) => s3StorageBackend(connection.client, connection.bucket) })
|
|
96
|
+
],
|
|
97
|
+
exports: [require_storage.StorageBackend]
|
|
98
|
+
});
|
|
99
|
+
//#endregion
|
|
100
|
+
exports.StorageConfig = StorageConfig;
|
|
101
|
+
exports.s3Schema = s3Schema;
|
|
102
|
+
exports.s3Storage = s3Storage;
|
|
103
|
+
exports.s3StorageBackend = s3StorageBackend;
|
package/dist/s3.d.cts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { a as StorageService, i as StorageBackend } from "./storage-C9j3zj7r.cjs";
|
|
2
|
+
import { Module, Scope } from "@btravstack/di";
|
|
3
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
|
+
import { ConfigInvalid, Env } from "@btravstack/config";
|
|
5
|
+
//#region src/s3.d.ts
|
|
6
|
+
declare const StorageConfig_base: import("@btravstack/di").PortClass<"StorageConfig">;
|
|
7
|
+
/** What the graph bound from the environment for the S3 adapter. */
|
|
8
|
+
declare class StorageConfig extends StorageConfig_base<{
|
|
9
|
+
readonly endpoint: string;
|
|
10
|
+
readonly region: string;
|
|
11
|
+
readonly bucket: string;
|
|
12
|
+
readonly accessKeyId: string;
|
|
13
|
+
readonly secretAccessKey: string;
|
|
14
|
+
}> {}
|
|
15
|
+
/**
|
|
16
|
+
* The five variables an S3-compatible store needs, and no sixth.
|
|
17
|
+
*
|
|
18
|
+
* `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
|
|
19
|
+
* this port exists for has one, and a default pointing at Amazon would be a
|
|
20
|
+
* surprising bill. **Path-style addressing is not configurable**, it is simply
|
|
21
|
+
* on โ every self-hosted store requires it and AWS accepts it, so it is a value
|
|
22
|
+
* that never changes.
|
|
23
|
+
*/
|
|
24
|
+
declare const s3Schema: import("@btravstack/config").ConfigSchema<Readonly<Record<string, string | undefined>>, {
|
|
25
|
+
readonly endpoint: string;
|
|
26
|
+
readonly region: string;
|
|
27
|
+
readonly bucket: string;
|
|
28
|
+
readonly accessKeyId: string;
|
|
29
|
+
readonly secretAccessKey: string;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* The adapter's service over a client and a bucket. A delete of a key nobody
|
|
33
|
+
* stored answers `Ok`, which is S3's own idempotent behaviour rather than a
|
|
34
|
+
* fiction layered over it.
|
|
35
|
+
*/
|
|
36
|
+
declare const s3StorageBackend: (client: S3Client, bucket: string) => StorageService;
|
|
37
|
+
/**
|
|
38
|
+
* The S3 adapter: one client, built with the scope and destroyed with it, and
|
|
39
|
+
* `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
|
|
40
|
+
* only through this subpath, so an application composing the memory adapter
|
|
41
|
+
* installs neither.
|
|
42
|
+
*/
|
|
43
|
+
declare const s3Storage: () => Module<StorageBackend, ConfigInvalid, Env | Scope>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { StorageConfig, s3Schema, s3Storage, s3StorageBackend };
|
package/dist/s3.d.mts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { a as StorageService, i as StorageBackend } from "./storage-C9j3zj7r.mjs";
|
|
2
|
+
import { Module, Scope } from "@btravstack/di";
|
|
3
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
4
|
+
import { ConfigInvalid, Env } from "@btravstack/config";
|
|
5
|
+
//#region src/s3.d.ts
|
|
6
|
+
declare const StorageConfig_base: import("@btravstack/di").PortClass<"StorageConfig">;
|
|
7
|
+
/** What the graph bound from the environment for the S3 adapter. */
|
|
8
|
+
declare class StorageConfig extends StorageConfig_base<{
|
|
9
|
+
readonly endpoint: string;
|
|
10
|
+
readonly region: string;
|
|
11
|
+
readonly bucket: string;
|
|
12
|
+
readonly accessKeyId: string;
|
|
13
|
+
readonly secretAccessKey: string;
|
|
14
|
+
}> {}
|
|
15
|
+
/**
|
|
16
|
+
* The five variables an S3-compatible store needs, and no sixth.
|
|
17
|
+
*
|
|
18
|
+
* `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
|
|
19
|
+
* this port exists for has one, and a default pointing at Amazon would be a
|
|
20
|
+
* surprising bill. **Path-style addressing is not configurable**, it is simply
|
|
21
|
+
* on โ every self-hosted store requires it and AWS accepts it, so it is a value
|
|
22
|
+
* that never changes.
|
|
23
|
+
*/
|
|
24
|
+
declare const s3Schema: import("@btravstack/config").ConfigSchema<Readonly<Record<string, string | undefined>>, {
|
|
25
|
+
readonly endpoint: string;
|
|
26
|
+
readonly region: string;
|
|
27
|
+
readonly bucket: string;
|
|
28
|
+
readonly accessKeyId: string;
|
|
29
|
+
readonly secretAccessKey: string;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* The adapter's service over a client and a bucket. A delete of a key nobody
|
|
33
|
+
* stored answers `Ok`, which is S3's own idempotent behaviour rather than a
|
|
34
|
+
* fiction layered over it.
|
|
35
|
+
*/
|
|
36
|
+
declare const s3StorageBackend: (client: S3Client, bucket: string) => StorageService;
|
|
37
|
+
/**
|
|
38
|
+
* The S3 adapter: one client, built with the scope and destroyed with it, and
|
|
39
|
+
* `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
|
|
40
|
+
* only through this subpath, so an application composing the memory adapter
|
|
41
|
+
* installs neither.
|
|
42
|
+
*/
|
|
43
|
+
declare const s3Storage: () => Module<StorageBackend, ConfigInvalid, Env | Scope>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { StorageConfig, s3Schema, s3Storage, s3StorageBackend };
|
package/dist/s3.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { a as StorageUnavailable, i as StorageBackend, t as ObjectNotFound } from "./storage-CRkwIhac.mjs";
|
|
2
|
+
import { Module, Port, Provider } from "@btravstack/di";
|
|
3
|
+
import { fromPromise, fromSafePromise } from "unthrown";
|
|
4
|
+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
5
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
6
|
+
import { Config, Env } from "@btravstack/config";
|
|
7
|
+
//#region src/s3.ts
|
|
8
|
+
/** What the graph bound from the environment for the S3 adapter. */
|
|
9
|
+
var StorageConfig = class extends Port("StorageConfig") {};
|
|
10
|
+
/**
|
|
11
|
+
* The five variables an S3-compatible store needs, and no sixth.
|
|
12
|
+
*
|
|
13
|
+
* `STORAGE_S3_ENDPOINT` is required rather than defaulted to AWS: every store
|
|
14
|
+
* this port exists for has one, and a default pointing at Amazon would be a
|
|
15
|
+
* surprising bill. **Path-style addressing is not configurable**, it is simply
|
|
16
|
+
* on โ every self-hosted store requires it and AWS accepts it, so it is a value
|
|
17
|
+
* that never changes.
|
|
18
|
+
*/
|
|
19
|
+
const s3Schema = Config.object({
|
|
20
|
+
endpoint: Config.string("STORAGE_S3_ENDPOINT"),
|
|
21
|
+
region: Config.string("STORAGE_S3_REGION", { default: "us-east-1" }),
|
|
22
|
+
bucket: Config.string("STORAGE_S3_BUCKET"),
|
|
23
|
+
accessKeyId: Config.string("STORAGE_S3_ACCESS_KEY_ID"),
|
|
24
|
+
secretAccessKey: Config.string("STORAGE_S3_SECRET_ACCESS_KEY")
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The client, as a port of its own: a resourceful provider is handed back the
|
|
28
|
+
* service it acquired, and a store's four methods are not something you can
|
|
29
|
+
* close โ so the client rides the graph and the scope closing destroys it.
|
|
30
|
+
*/
|
|
31
|
+
var S3Connection = class extends Port("S3Connection") {};
|
|
32
|
+
const unavailable = (operation, key, cause) => new StorageUnavailable({
|
|
33
|
+
operation,
|
|
34
|
+
key,
|
|
35
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
36
|
+
});
|
|
37
|
+
/** A missing key is S3's `NoSuchKey`, which is a different fact from "the store is down". */
|
|
38
|
+
const isMissing = (cause) => cause instanceof Error && (cause.name === "NoSuchKey" || cause.name === "NotFound");
|
|
39
|
+
/**
|
|
40
|
+
* The adapter's service over a client and a bucket. A delete of a key nobody
|
|
41
|
+
* stored answers `Ok`, which is S3's own idempotent behaviour rather than a
|
|
42
|
+
* fiction layered over it.
|
|
43
|
+
*/
|
|
44
|
+
const s3StorageBackend = (client, bucket) => ({
|
|
45
|
+
put: (key, bytes, options) => fromPromise(client.send(new PutObjectCommand({
|
|
46
|
+
Bucket: bucket,
|
|
47
|
+
Key: key,
|
|
48
|
+
Body: bytes,
|
|
49
|
+
ContentType: options.contentType
|
|
50
|
+
})), (cause) => unavailable("put", key, cause)).map(() => void 0),
|
|
51
|
+
get: (key) => fromPromise(client.send(new GetObjectCommand({
|
|
52
|
+
Bucket: bucket,
|
|
53
|
+
Key: key
|
|
54
|
+
})).then(async (response) => ({
|
|
55
|
+
bytes: await response.Body.transformToByteArray(),
|
|
56
|
+
contentType: response.ContentType ?? "application/octet-stream"
|
|
57
|
+
})), (cause) => isMissing(cause) ? new ObjectNotFound({ key }) : unavailable("get", key, cause)),
|
|
58
|
+
delete: (key) => fromPromise(client.send(new DeleteObjectCommand({
|
|
59
|
+
Bucket: bucket,
|
|
60
|
+
Key: key
|
|
61
|
+
})), (cause) => unavailable("delete", key, cause)).map(() => void 0),
|
|
62
|
+
presignedUrl: (key, options) => fromPromise(getSignedUrl(client, new GetObjectCommand({
|
|
63
|
+
Bucket: bucket,
|
|
64
|
+
Key: key
|
|
65
|
+
}), { expiresIn: Math.ceil(options.ttlMs / 1e3) }), (cause) => unavailable("presignedUrl", key, cause))
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The S3 adapter: one client, built with the scope and destroyed with it, and
|
|
69
|
+
* `StorageBackend` over it. Its two AWS SDK peers are **optional** and reached
|
|
70
|
+
* only through this subpath, so an application composing the memory adapter
|
|
71
|
+
* installs neither.
|
|
72
|
+
*/
|
|
73
|
+
const s3Storage = () => Module("S3Storage")({
|
|
74
|
+
needs: [Env],
|
|
75
|
+
provides: [
|
|
76
|
+
Config.provider(StorageConfig)(s3Schema),
|
|
77
|
+
Provider(S3Connection)({ config: StorageConfig }, {
|
|
78
|
+
acquire: ({ config }) => fromSafePromise(Promise.resolve({
|
|
79
|
+
client: new S3Client({
|
|
80
|
+
endpoint: config.endpoint,
|
|
81
|
+
region: config.region,
|
|
82
|
+
forcePathStyle: true,
|
|
83
|
+
credentials: {
|
|
84
|
+
accessKeyId: config.accessKeyId,
|
|
85
|
+
secretAccessKey: config.secretAccessKey
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
bucket: config.bucket
|
|
89
|
+
})),
|
|
90
|
+
release: ({ client }) => {
|
|
91
|
+
client.destroy();
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
Provider(StorageBackend)({ connection: S3Connection }, { sync: ({ connection }) => s3StorageBackend(connection.client, connection.bucket) })
|
|
95
|
+
],
|
|
96
|
+
exports: [StorageBackend]
|
|
97
|
+
});
|
|
98
|
+
//#endregion
|
|
99
|
+
export { StorageConfig, s3Schema, s3Storage, s3StorageBackend };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AsyncResult } from "unthrown";
|
|
2
|
+
//#region src/storage.d.ts
|
|
3
|
+
/** One stored object: its bytes and what they are. */
|
|
4
|
+
type StoredObject = {
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
readonly contentType: string;
|
|
7
|
+
};
|
|
8
|
+
declare const ObjectNotFound_base: import("unthrown").TaggedErrorConstructor<"ObjectNotFound">;
|
|
9
|
+
/** The key is not there. Only `get` answers it โ see `delete` and `presignedUrl` below. */
|
|
10
|
+
declare class ObjectNotFound extends ObjectNotFound_base<{
|
|
11
|
+
readonly key: string;
|
|
12
|
+
}> {}
|
|
13
|
+
declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
|
|
14
|
+
/** The store could not answer โ the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
15
|
+
declare class StorageUnavailable extends StorageUnavailable_base<{
|
|
16
|
+
readonly operation: "put" | "get" | "delete" | "presignedUrl";
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
}> {}
|
|
20
|
+
declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructor<"PresignNotSupported">;
|
|
21
|
+
/** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
|
|
22
|
+
declare class PresignNotSupported extends PresignNotSupported_base<{
|
|
23
|
+
readonly key: string;
|
|
24
|
+
}> {}
|
|
25
|
+
type StorageService = {
|
|
26
|
+
readonly put: (key: string, bytes: Uint8Array, options: {
|
|
27
|
+
readonly contentType: string;
|
|
28
|
+
}) => AsyncResult<void, StorageUnavailable>;
|
|
29
|
+
readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
|
|
30
|
+
/** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
|
|
31
|
+
readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
|
|
32
|
+
/**
|
|
33
|
+
* A URL that reads the object for `ttlMs`, without the caller holding
|
|
34
|
+
* credentials.
|
|
35
|
+
*
|
|
36
|
+
* There is **no `ObjectNotFound` arm**: presigning is a signature computation
|
|
37
|
+
* that asks the store nothing, so a URL for an absent key is minted happily
|
|
38
|
+
* and 404s when followed. Checking would cost a HEAD per call.
|
|
39
|
+
*/
|
|
40
|
+
readonly presignedUrl: (key: string, options: {
|
|
41
|
+
readonly ttlMs: number;
|
|
42
|
+
}) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
|
|
43
|
+
};
|
|
44
|
+
declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
|
|
45
|
+
/**
|
|
46
|
+
* The port an application depends on.
|
|
47
|
+
*
|
|
48
|
+
* Keys are plain strings the caller composes, tenant included: a store is an
|
|
49
|
+
* application service, and the framework has no concept of a tenant to put in a
|
|
50
|
+
* slot.
|
|
51
|
+
*
|
|
52
|
+
* **Bytes, not streams.** An object here is a document. Streaming would change
|
|
53
|
+
* every signature, adapter and test to serve a case that wants a different
|
|
54
|
+
* design anyway โ a stated non-goal, not an oversight.
|
|
55
|
+
*/
|
|
56
|
+
declare class Storage extends Storage_base<StorageService> {}
|
|
57
|
+
declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBackend">;
|
|
58
|
+
/** The port every adapter provides, and the one an application never depends on. */
|
|
59
|
+
declare class StorageBackend extends StorageBackend_base<StorageService> {}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { StorageService as a, StorageBackend as i, PresignNotSupported as n, StorageUnavailable as o, Storage as r, StoredObject as s, ObjectNotFound as t };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AsyncResult } from "unthrown";
|
|
2
|
+
//#region src/storage.d.ts
|
|
3
|
+
/** One stored object: its bytes and what they are. */
|
|
4
|
+
type StoredObject = {
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
readonly contentType: string;
|
|
7
|
+
};
|
|
8
|
+
declare const ObjectNotFound_base: import("unthrown").TaggedErrorConstructor<"ObjectNotFound">;
|
|
9
|
+
/** The key is not there. Only `get` answers it โ see `delete` and `presignedUrl` below. */
|
|
10
|
+
declare class ObjectNotFound extends ObjectNotFound_base<{
|
|
11
|
+
readonly key: string;
|
|
12
|
+
}> {}
|
|
13
|
+
declare const StorageUnavailable_base: import("unthrown").TaggedErrorConstructor<"StorageUnavailable">;
|
|
14
|
+
/** The store could not answer โ the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
15
|
+
declare class StorageUnavailable extends StorageUnavailable_base<{
|
|
16
|
+
readonly operation: "put" | "get" | "delete" | "presignedUrl";
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
}> {}
|
|
20
|
+
declare const PresignNotSupported_base: import("unthrown").TaggedErrorConstructor<"PresignNotSupported">;
|
|
21
|
+
/** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
|
|
22
|
+
declare class PresignNotSupported extends PresignNotSupported_base<{
|
|
23
|
+
readonly key: string;
|
|
24
|
+
}> {}
|
|
25
|
+
type StorageService = {
|
|
26
|
+
readonly put: (key: string, bytes: Uint8Array, options: {
|
|
27
|
+
readonly contentType: string;
|
|
28
|
+
}) => AsyncResult<void, StorageUnavailable>;
|
|
29
|
+
readonly get: (key: string) => AsyncResult<StoredObject, ObjectNotFound | StorageUnavailable>;
|
|
30
|
+
/** Deleting a key nobody stored is `Ok`: delete is idempotent, which is S3's own behaviour rather than a fiction over it. */
|
|
31
|
+
readonly delete: (key: string) => AsyncResult<void, StorageUnavailable>;
|
|
32
|
+
/**
|
|
33
|
+
* A URL that reads the object for `ttlMs`, without the caller holding
|
|
34
|
+
* credentials.
|
|
35
|
+
*
|
|
36
|
+
* There is **no `ObjectNotFound` arm**: presigning is a signature computation
|
|
37
|
+
* that asks the store nothing, so a URL for an absent key is minted happily
|
|
38
|
+
* and 404s when followed. Checking would cost a HEAD per call.
|
|
39
|
+
*/
|
|
40
|
+
readonly presignedUrl: (key: string, options: {
|
|
41
|
+
readonly ttlMs: number;
|
|
42
|
+
}) => AsyncResult<string, PresignNotSupported | StorageUnavailable>;
|
|
43
|
+
};
|
|
44
|
+
declare const Storage_base: import("@btravstack/di").PortClass<"Storage">;
|
|
45
|
+
/**
|
|
46
|
+
* The port an application depends on.
|
|
47
|
+
*
|
|
48
|
+
* Keys are plain strings the caller composes, tenant included: a store is an
|
|
49
|
+
* application service, and the framework has no concept of a tenant to put in a
|
|
50
|
+
* slot.
|
|
51
|
+
*
|
|
52
|
+
* **Bytes, not streams.** An object here is a document. Streaming would change
|
|
53
|
+
* every signature, adapter and test to serve a case that wants a different
|
|
54
|
+
* design anyway โ a stated non-goal, not an oversight.
|
|
55
|
+
*/
|
|
56
|
+
declare class Storage extends Storage_base<StorageService> {}
|
|
57
|
+
declare const StorageBackend_base: import("@btravstack/di").PortClass<"StorageBackend">;
|
|
58
|
+
/** The port every adapter provides, and the one an application never depends on. */
|
|
59
|
+
declare class StorageBackend extends StorageBackend_base<StorageService> {}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { StorageService as a, StorageBackend as i, PresignNotSupported as n, StorageUnavailable as o, Storage as r, StoredObject as s, ObjectNotFound as t };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Port } from "@btravstack/di";
|
|
2
|
+
import { TaggedError } from "unthrown";
|
|
3
|
+
//#region src/storage.ts
|
|
4
|
+
/** The key is not there. Only `get` answers it โ see `delete` and `presignedUrl` below. */
|
|
5
|
+
var ObjectNotFound = class extends TaggedError("ObjectNotFound") {};
|
|
6
|
+
/** The store could not answer โ the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
7
|
+
var StorageUnavailable = class extends TaggedError("StorageUnavailable") {};
|
|
8
|
+
/** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
|
|
9
|
+
var PresignNotSupported = class extends TaggedError("PresignNotSupported") {};
|
|
10
|
+
/**
|
|
11
|
+
* The port an application depends on.
|
|
12
|
+
*
|
|
13
|
+
* Keys are plain strings the caller composes, tenant included: a store is an
|
|
14
|
+
* application service, and the framework has no concept of a tenant to put in a
|
|
15
|
+
* slot.
|
|
16
|
+
*
|
|
17
|
+
* **Bytes, not streams.** An object here is a document. Streaming would change
|
|
18
|
+
* every signature, adapter and test to serve a case that wants a different
|
|
19
|
+
* design anyway โ a stated non-goal, not an oversight.
|
|
20
|
+
*/
|
|
21
|
+
var Storage = class extends Port("Storage") {};
|
|
22
|
+
/** The port every adapter provides, and the one an application never depends on. */
|
|
23
|
+
var StorageBackend = class extends Port("StorageBackend") {};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { StorageUnavailable as a, StorageBackend as i, PresignNotSupported as n, Storage as r, ObjectNotFound as t };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
let _btravstack_di = require("@btravstack/di");
|
|
2
|
+
let unthrown = require("unthrown");
|
|
3
|
+
//#region src/storage.ts
|
|
4
|
+
/** The key is not there. Only `get` answers it โ see `delete` and `presignedUrl` below. */
|
|
5
|
+
var ObjectNotFound = class extends (0, unthrown.TaggedError)("ObjectNotFound") {};
|
|
6
|
+
/** The store could not answer โ the endpoint is down, the credentials were refused, the bucket is gone. */
|
|
7
|
+
var StorageUnavailable = class extends (0, unthrown.TaggedError)("StorageUnavailable") {};
|
|
8
|
+
/** The adapter cannot mint a time-limited URL, and says so rather than pretending. */
|
|
9
|
+
var PresignNotSupported = class extends (0, unthrown.TaggedError)("PresignNotSupported") {};
|
|
10
|
+
/**
|
|
11
|
+
* The port an application depends on.
|
|
12
|
+
*
|
|
13
|
+
* Keys are plain strings the caller composes, tenant included: a store is an
|
|
14
|
+
* application service, and the framework has no concept of a tenant to put in a
|
|
15
|
+
* slot.
|
|
16
|
+
*
|
|
17
|
+
* **Bytes, not streams.** An object here is a document. Streaming would change
|
|
18
|
+
* every signature, adapter and test to serve a case that wants a different
|
|
19
|
+
* design anyway โ a stated non-goal, not an oversight.
|
|
20
|
+
*/
|
|
21
|
+
var Storage = class extends (0, _btravstack_di.Port)("Storage") {};
|
|
22
|
+
/** The port every adapter provides, and the one an application never depends on. */
|
|
23
|
+
var StorageBackend = class extends (0, _btravstack_di.Port)("StorageBackend") {};
|
|
24
|
+
//#endregion
|
|
25
|
+
Object.defineProperty(exports, "ObjectNotFound", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
get: function() {
|
|
28
|
+
return ObjectNotFound;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(exports, "PresignNotSupported", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
get: function() {
|
|
34
|
+
return PresignNotSupported;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(exports, "Storage", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
get: function() {
|
|
40
|
+
return Storage;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(exports, "StorageBackend", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
get: function() {
|
|
46
|
+
return StorageBackend;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(exports, "StorageUnavailable", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
get: function() {
|
|
52
|
+
return StorageUnavailable;
|
|
53
|
+
}
|
|
54
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@btravstack/storage",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "A Storage port for @btravstack/core: an in-memory adapter, an S3-compatible adapter with presigned reads, and a composition that spans, counts and logs every operation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dependency-injection",
|
|
7
|
+
"object-storage",
|
|
8
|
+
"s3",
|
|
9
|
+
"storage",
|
|
10
|
+
"typescript",
|
|
11
|
+
"unthrown"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/btravstack/btravstack#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/btravstack/btravstack/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Benoit TRAVERS <benoit.travers.fr@gmail.com>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/btravstack/btravstack.git",
|
|
22
|
+
"directory": "packages/storage"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.mjs",
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.mts",
|
|
36
|
+
"default": "./dist/index.mjs"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"./s3": {
|
|
44
|
+
"import": {
|
|
45
|
+
"types": "./dist/s3.d.mts",
|
|
46
|
+
"default": "./dist/s3.mjs"
|
|
47
|
+
},
|
|
48
|
+
"require": {
|
|
49
|
+
"types": "./dist/s3.d.cts",
|
|
50
|
+
"default": "./dist/s3.cjs"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./package.json": "./package.json"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@aws-sdk/client-s3": "3.1117.0",
|
|
57
|
+
"@aws-sdk/s3-request-presigner": "3.1117.0",
|
|
58
|
+
"@btravstack/tsconfig": "0.2.0",
|
|
59
|
+
"@opentelemetry/api": "1.9.1",
|
|
60
|
+
"@opentelemetry/sdk-metrics": "2.10.0",
|
|
61
|
+
"@opentelemetry/sdk-node": "0.221.0",
|
|
62
|
+
"@opentelemetry/sdk-trace": "2.10.0",
|
|
63
|
+
"@types/node": "26.2.0",
|
|
64
|
+
"@unthrown/vitest": "5.7.0",
|
|
65
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
66
|
+
"tsdown": "0.22.14",
|
|
67
|
+
"typescript": "7.0.2",
|
|
68
|
+
"unthrown": "5.7.0",
|
|
69
|
+
"vitest": "4.1.11",
|
|
70
|
+
"@btravstack/config": "0.3.0",
|
|
71
|
+
"@btravstack/internal-test-infra": "0.0.0",
|
|
72
|
+
"@btravstack/core": "0.3.0",
|
|
73
|
+
"@btravstack/observability": "0.3.0",
|
|
74
|
+
"@btravstack/di": "0.3.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"@aws-sdk/client-s3": "^3.1000.0",
|
|
78
|
+
"@aws-sdk/s3-request-presigner": "^3.1000.0",
|
|
79
|
+
"unthrown": "^5.0.0",
|
|
80
|
+
"@btravstack/config": "^0.3.0",
|
|
81
|
+
"@btravstack/di": "^0.3.0",
|
|
82
|
+
"@btravstack/core": "^0.3.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"@aws-sdk/client-s3": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@aws-sdk/s3-request-presigner": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"engines": {
|
|
93
|
+
"node": ">=20"
|
|
94
|
+
},
|
|
95
|
+
"scripts": {
|
|
96
|
+
"build": "tsdown src/index.ts src/s3.ts --format cjs,esm --dts --clean",
|
|
97
|
+
"dev": "tsdown src/index.ts src/s3.ts --format cjs,esm --dts --watch",
|
|
98
|
+
"test": "vitest run --coverage",
|
|
99
|
+
"test:types": "tsc -p tsconfig.test-d.json --noEmit",
|
|
100
|
+
"typecheck": "tsc --noEmit"
|
|
101
|
+
}
|
|
102
|
+
}
|