@storyshelf/storage-s3 0.1.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/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @storyshelf/storage-s3
2
+
3
+ The S3-compatible storage adapter for StoryShelf: reads and writes blobs to any S3-compatible object store, including AWS S3, Cloudflare R2, MinIO, and DigitalOcean Spaces.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/storage-s3
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/storage-s3
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createS3Storage } from "@storyshelf/storage-s3";
21
+ import { createShelfRouter } from "@storyshelf/core";
22
+
23
+ const storage = createS3Storage({
24
+ bucket: "my-shelf",
25
+ prefix: "storyshelf",
26
+ endpoint: process.env.S3_ENDPOINT, // optional, e.g. for MinIO/R2
27
+ region: "us-east-1", // optional
28
+ });
29
+
30
+ const app = createShelfRouter({ database, storage });
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### `S3StorageOptions`
36
+
37
+ ```ts
38
+ interface S3StorageOptions {
39
+ bucket: string; // required bucket name
40
+ prefix?: string; // optional key prefix, defaults to ""
41
+ endpoint?: string; // optional custom endpoint (MinIO, R2, etc.)
42
+ region?: string; // optional region, defaults to "us-east-1"
43
+ }
44
+ ```
45
+
46
+ ### `createS3Storage(options: S3StorageOptions): StorageAdapter`
47
+
48
+ Creates an S3 client (with `forcePathStyle` enabled for compatibility with MinIO/R2) and returns a `StorageAdapter`. The returned adapter implements every method of the `StorageAdapter` interface (`read`, `write`, `delete`, `exists`, `list(prefix)`).
49
+
50
+ ### `s3Key(prefix: string, path: string): string`
51
+
52
+ Helper that joins a storage prefix with a relative path into an object key.
53
+
54
+ ## How it fits in
55
+
56
+ `storage-s3` is the `storage` option for `createShelfRouter` in cloud or multi-node deployments. It implements the same `StorageAdapter` interface as `@storyshelf/storage-local`, so switching between local disk and object storage requires no changes elsewhere.
57
+
58
+ See `docs/architecture.md` and ADR 0006.
@@ -0,0 +1,29 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { S3Client } from "@aws-sdk/client-s3";
4
+ import { StorageAdapter } from "@storyshelf/core/adapter/storage";
5
+ //#region src/index.d.ts
6
+ /** Options for configuring an S3-compatible storage adapter. */
7
+ interface S3StorageOptions {
8
+ /** S3 bucket name. */
9
+ bucket: string;
10
+ /** Optional key prefix applied to all stored objects. */
11
+ prefix?: string;
12
+ /** Custom endpoint for S3-compatible services (e.g. MinIO, R2). */
13
+ endpoint?: string;
14
+ /** AWS region. Defaults to `us-east-1`. */
15
+ region?: string;
16
+ /** Pre-configured S3 client. Defaults to a client built from the other options. */
17
+ client?: S3Client;
18
+ }
19
+ declare function s3Key(prefix: string, path: string): string;
20
+ /**
21
+ * Create an S3-compatible StorageAdapter (AWS S3, R2, MinIO, etc.).
22
+ *
23
+ * @param options - S3 configuration options.
24
+ * @returns A StorageAdapter backed by the configured S3 bucket.
25
+ */
26
+ declare function createS3Storage(options: S3StorageOptions): StorageAdapter;
27
+ //#endregion
28
+ export { S3StorageOptions, createS3Storage, s3Key };
29
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,79 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
5
+ //#region src/index.ts
6
+ function s3Key(prefix, path) {
7
+ return prefix === "" ? path : `${prefix}/${path}`;
8
+ }
9
+ function s3Rel(prefix, key) {
10
+ return prefix === "" ? key : key.slice(prefix.length + 1);
11
+ }
12
+ function isNotFound(error) {
13
+ return error instanceof S3ServiceException && error.$metadata.httpStatusCode === 404;
14
+ }
15
+ /**
16
+ * Create an S3-compatible StorageAdapter (AWS S3, R2, MinIO, etc.).
17
+ *
18
+ * @param options - S3 configuration options.
19
+ * @returns A StorageAdapter backed by the configured S3 bucket.
20
+ */
21
+ function createS3Storage(options) {
22
+ const { bucket, prefix = "", endpoint, region = "us-east-1", client: injectedClient } = options;
23
+ const client = injectedClient ?? new S3Client({
24
+ endpoint,
25
+ region,
26
+ forcePathStyle: true
27
+ });
28
+ return {
29
+ metadata: {
30
+ name: "S3 Storage",
31
+ version: "0.1.0",
32
+ description: "S3-compatible storage adapter",
33
+ kind: "s3"
34
+ },
35
+ async read(path) {
36
+ const body = (await client.send(new GetObjectCommand({
37
+ Bucket: bucket,
38
+ Key: s3Key(prefix, path)
39
+ }))).Body;
40
+ if (body === void 0) return Buffer.alloc(0);
41
+ return Buffer.from(await body.transformToByteArray());
42
+ },
43
+ async write(path, data) {
44
+ await client.send(new PutObjectCommand({
45
+ Bucket: bucket,
46
+ Key: s3Key(prefix, path),
47
+ Body: data
48
+ }));
49
+ },
50
+ async delete(path) {
51
+ await client.send(new DeleteObjectCommand({
52
+ Bucket: bucket,
53
+ Key: s3Key(prefix, path)
54
+ }));
55
+ },
56
+ async exists(path) {
57
+ try {
58
+ await client.send(new HeadObjectCommand({
59
+ Bucket: bucket,
60
+ Key: s3Key(prefix, path)
61
+ }));
62
+ return true;
63
+ } catch (error) {
64
+ if (isNotFound(error)) return false;
65
+ throw error;
66
+ }
67
+ },
68
+ async list(listPrefix) {
69
+ return ((await client.send(new ListObjectsV2Command({
70
+ Bucket: bucket,
71
+ Prefix: s3Key(prefix, listPrefix)
72
+ }))).Contents ?? []).map((item) => item.Key).filter((key) => key !== void 0).map((key) => s3Rel(prefix, key));
73
+ }
74
+ };
75
+ }
76
+ //#endregion
77
+ export { createS3Storage, s3Key };
78
+
79
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import {\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand,\n S3Client,\n S3ServiceException,\n} from \"@aws-sdk/client-s3\";\n\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\n\ndeclare const __PKG_VERSION__: string;\n\n/** Options for configuring an S3-compatible storage adapter. */\nexport interface S3StorageOptions {\n /** S3 bucket name. */\n bucket: string;\n /** Optional key prefix applied to all stored objects. */\n prefix?: string;\n /** Custom endpoint for S3-compatible services (e.g. MinIO, R2). */\n endpoint?: string;\n /** AWS region. Defaults to `us-east-1`. */\n region?: string;\n /** Pre-configured S3 client. Defaults to a client built from the other options. */\n client?: S3Client;\n}\n\nexport function s3Key(prefix: string, path: string): string {\n return prefix === \"\" ? path : `${prefix}/${path}`;\n}\n\nfunction s3Rel(prefix: string, key: string): string {\n return prefix === \"\" ? key : key.slice(prefix.length + 1);\n}\n\nfunction isNotFound(error: unknown): boolean {\n return error instanceof S3ServiceException && error.$metadata.httpStatusCode === 404;\n}\n\n/**\n * Create an S3-compatible StorageAdapter (AWS S3, R2, MinIO, etc.).\n *\n * @param options - S3 configuration options.\n * @returns A StorageAdapter backed by the configured S3 bucket.\n */\nexport function createS3Storage(options: S3StorageOptions): StorageAdapter {\n const { bucket, prefix = \"\", endpoint, region = \"us-east-1\", client: injectedClient } = options;\n const client = injectedClient ?? new S3Client({ endpoint, region, forcePathStyle: true });\n\n return {\n metadata: {\n name: \"S3 Storage\",\n version: typeof __PKG_VERSION__ === \"undefined\" ? \"0.0.0\" : __PKG_VERSION__, // oxlint-disable-line unicorn/no-typeof-undefined\n description: \"S3-compatible storage adapter\",\n kind: \"s3\",\n },\n async read(path) {\n const response = await client.send(\n new GetObjectCommand({ Bucket: bucket, Key: s3Key(prefix, path) }),\n );\n const body = response.Body;\n if (body === undefined) {\n return Buffer.alloc(0);\n }\n return Buffer.from(await body.transformToByteArray());\n },\n async write(path, data) {\n await client.send(\n new PutObjectCommand({ Bucket: bucket, Key: s3Key(prefix, path), Body: data }),\n );\n },\n async delete(path) {\n await client.send(\n new DeleteObjectCommand({ Bucket: bucket, Key: s3Key(prefix, path) }),\n );\n },\n async exists(path) {\n try {\n await client.send(new HeadObjectCommand({ Bucket: bucket, Key: s3Key(prefix, path) }));\n return true;\n } catch (error) {\n if (isNotFound(error)) {\n return false;\n }\n throw error;\n }\n },\n async list(listPrefix) {\n const response = await client.send(\n new ListObjectsV2Command({ Bucket: bucket, Prefix: s3Key(prefix, listPrefix) }),\n );\n return (response.Contents ?? [])\n .map((item) => item.Key)\n .filter((key): key is string => key !== undefined)\n .map((key) => s3Rel(prefix, key));\n },\n };\n}\n"],"mappings":";;;;;AA4BA,SAAgB,MAAM,QAAgB,MAAsB;CAC1D,OAAO,WAAW,KAAK,OAAO,GAAG,OAAO,GAAG;AAC7C;AAEA,SAAS,MAAM,QAAgB,KAAqB;CAClD,OAAO,WAAW,KAAK,MAAM,IAAI,MAAM,OAAO,SAAS,CAAC;AAC1D;AAEA,SAAS,WAAW,OAAyB;CAC3C,OAAO,iBAAiB,sBAAsB,MAAM,UAAU,mBAAmB;AACnF;;;;;;;AAQA,SAAgB,gBAAgB,SAA2C;CACzE,MAAM,EAAE,QAAQ,SAAS,IAAI,UAAU,SAAS,aAAa,QAAQ,mBAAmB;CACxF,MAAM,SAAS,kBAAkB,IAAI,SAAS;EAAE;EAAU;EAAQ,gBAAgB;CAAK,CAAC;CAExF,OAAO;EACL,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;EACR;EACA,MAAM,KAAK,MAAM;GAIf,MAAM,QAAO,MAHU,OAAO,KAC5B,IAAI,iBAAiB;IAAE,QAAQ;IAAQ,KAAK,MAAM,QAAQ,IAAI;GAAE,CAAC,CACnE,EAAA,CACsB;GACtB,IAAI,SAAS,KAAA,GACX,OAAO,OAAO,MAAM,CAAC;GAEvB,OAAO,OAAO,KAAK,MAAM,KAAK,qBAAqB,CAAC;EACtD;EACA,MAAM,MAAM,MAAM,MAAM;GACtB,MAAM,OAAO,KACX,IAAI,iBAAiB;IAAE,QAAQ;IAAQ,KAAK,MAAM,QAAQ,IAAI;IAAG,MAAM;GAAK,CAAC,CAC/E;EACF;EACA,MAAM,OAAO,MAAM;GACjB,MAAM,OAAO,KACX,IAAI,oBAAoB;IAAE,QAAQ;IAAQ,KAAK,MAAM,QAAQ,IAAI;GAAE,CAAC,CACtE;EACF;EACA,MAAM,OAAO,MAAM;GACjB,IAAI;IACF,MAAM,OAAO,KAAK,IAAI,kBAAkB;KAAE,QAAQ;KAAQ,KAAK,MAAM,QAAQ,IAAI;IAAE,CAAC,CAAC;IACrF,OAAO;GACT,SAAS,OAAO;IACd,IAAI,WAAW,KAAK,GAClB,OAAO;IAET,MAAM;GACR;EACF;EACA,MAAM,KAAK,YAAY;GAIrB,SAAQ,MAHe,OAAO,KAC5B,IAAI,qBAAqB;IAAE,QAAQ;IAAQ,QAAQ,MAAM,QAAQ,UAAU;GAAE,CAAC,CAChF,EAAA,CACiB,YAAY,CAAC,EAAA,CAC3B,KAAK,SAAS,KAAK,GAAG,CAAC,CACvB,QAAQ,QAAuB,QAAQ,KAAA,CAAS,CAAC,CACjD,KAAK,QAAQ,MAAM,QAAQ,GAAG,CAAC;EACpC;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@storyshelf/storage-s3",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "S3-compatible storage adapter for StoryShelf (AWS SDK v3).",
6
+ "author": {
7
+ "name": "Siddhant Gupta",
8
+ "url": "https://guptasiddhant.com"
9
+ },
10
+ "license": "MIT",
11
+ "sideEffects": false,
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/GuptaSiddhant/storyshelf.git",
15
+ "directory": "packages/storage-s3"
16
+ },
17
+ "homepage": "https://github.com/GuptaSiddhant/storyshelf#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/GuptaSiddhant/storyshelf/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "exports": {
24
+ ".": "./dist/index.mjs",
25
+ "./package.json": "./package.json"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsdown",
33
+ "dev": "tsdown -w",
34
+ "fmt": "oxfmt -c ../../.oxfmtrc.json ./src",
35
+ "lint": "oxlint --type-aware --type-check ./src",
36
+ "test": "vitest run",
37
+ "prepublishOnly": "nub run build"
38
+ },
39
+ "dependencies": {
40
+ "@aws-sdk/client-s3": "^3.700.0",
41
+ "@storyshelf/core": "workspace:*"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "catalog:",
45
+ "@vitest/coverage-v8": "catalog:",
46
+ "oxfmt": "catalog:",
47
+ "oxlint": "catalog:",
48
+ "oxlint-tsgolint": "catalog:",
49
+ "tsdown": "catalog:",
50
+ "typescript": "catalog:",
51
+ "vitest": "catalog:"
52
+ },
53
+ "types": "./dist/index.d.mts",
54
+ "exports": {
55
+ ".": {
56
+ "source": "./src/index.ts",
57
+ "default": "./dist/index.mjs"
58
+ },
59
+ "./package.json": "./package.json"
60
+ }
61
+ }